很多年前。
我们想“废弃一个函数”。
第一反应就是删掉。
但你删一次。
就会有人跑来敲你。
“我升级了库,项目编不过了。”
更要命的是。
那时候很多库就是一堆头文件。
接口发出去。
就像把螺丝刀借给全公司。
你收不回。
所以旧接口只能留着。
问题就变成另一件事。
怎么让人别再用它。
这里说的“接口”。
就是别人会拿来用的东西。
可能是一个函数。
也可能是一个类型。
再朴素点说。
就是你头文件里写的那些名字。
当年没有它的时候,我们怎么劝人别用旧接口
当年最常见的办法,就是写注释。 你把“别用这个”贴在声明旁边,指望下一个人能看到。
结果往往是:认真读代码的人会看到。 更快的人,早就复制粘贴走了。
// deprecated: use foo2()
int foo();
但注释只对“愿意看注释的人”有效。 工程里最危险的那位,往往就是复制粘贴最快的那位。
再往后,大家开始上宏。 因为你想让编译器出声提醒,而不是靠人自觉。
#if defined(_MSC_VER)
# define DEPRECATED __declspec(deprecated)
#elif defined(__GNUC__)
# define DEPRECATED __attribute__((deprecated))
#else
# define DEPRECATED
#endif
DEPRECATED int foo();
能用。 但你看着就累。
更麻烦的是宏一旦写错,最恐怖的不是报错,是“静默失败”。 你以为大家会收到提醒,结果谁都没收到。
你写了个“退役公告”。 但没有人看见。
在它进标准之前:编译器已经各玩各的
其实“让编译器提醒”这件事,早就有人做了。 只是大家各用各的写法,代码一搬家就要重写。
MSVC 这边是 __declspec(...)。
GCC/Clang 这边是 __attribute__((...))。
__declspec(deprecated) int foo();
__attribute__((deprecated)) int foo();
你能看出来,它们本质上就是把一个“标签”贴在声明上。 问题在于:这不是 C++ 语言的一部分。
它是编译器私货。 所以你只能用宏把它们粘起来,也就有了上一节那个“写错就静默失败”的坑。
横向看一眼:四种办法都是什么味道
注释最省事。 但它的提醒范围,基本等于“愿意读注释的人”。
宏能让编译器出声,代价是代码像胶带一样越贴越多。 平台一多,就开始翻车。
编译器扩展更直接也更强。 但你一换编译器,写法就得跟着换。
标准属性没魔法。 它只是把大家早就在做的事,统一成一个写法。
C++14:把这事变成语言的一部分
C++14 给了一个更直接的办法:属性(attribute)。
你可以把它理解成“贴在声明旁边、给编译器看的标签”。
写法就是 [[...]]。
[[deprecated]]
int foo();
它不会改变函数怎么运行。
它做的事很单纯。
谁在代码里调用 foo(),编译器就给谁一个 warning。
这就像你给旧接口贴了一张黄牌。 “你还可以用,但你得知道自己在冒险。”
warning 到底是什么
warning 是编译器在“翻译代码”时说的话。 代码还没跑起来,它就已经把问题指出来了。
你可以把它当成一种“有风险,但还没到立刻爆炸”的提示。 比如类型不匹配、可能漏掉返回值,或者这里这种“你在用已废弃的接口”。
有些团队为什么会怕 warning
因为有些团队会把 warning 当成 error。
他们会打开一个选项(比如 -Werror)。
让 warning 直接变成“编不过”。
这招对质量很有效。 但对迁移也很凶。
你一贴上 [[deprecated]]。
整个 CI 可能立刻红一片。
所以 [[deprecated]] 更像黄牌。
你先让风险可见,再给团队一点时间把路修好。
还能带一句话
你还可以顺手写一句理由。
这句话会跟着 warning 一起出现。 比写在 wiki 里靠谱。
[[deprecated("use foo2()")]]
int foo();
int foo2();
warning 大概长这样
不同编译器的口气不一样。
但意思差不多。
warning: 'foo' is deprecated: use foo2() [-Wdeprecated-declarations]
你看到它,就知道“有人在用旧路”。
这套写法不是 C++ 独创
更早的语言里,其实也有类似的“废弃标记”。 做的事情都差不多:把“别再用这个”从文档,搬到编译器能看到的地方。
Java 里会写 @Deprecated。
@Deprecated
int foo() { return 0; }
C# 里常见的是 [Obsolete]。
[Obsolete("use Foo2")]
int Foo() => 0;
C++ 的选择是 [[...]]。
它来自 C++11 的属性语法。
目标是给标准库和工程一个“跨编译器的统一写法”。
但有一点不一样。
Java/C# 那套标记。 很多时候是给“框架”看的。
程序跑起来以后。 它们还能被反射读出来。
C++ 的属性更像是给“编译器和工具链”看的。 你一般不会在运行时去读取它。
为什么 C++ 最后用了 [[...]]
因为 C++ 背着太多老代码。
你很难再塞进一个新的关键字,比如 deprecated。
一旦有人在旧代码里用过这个名字,标准就会把他砸死。
[[...]] 这种写法比较“绕”。
但它有个好处。
它在老编译器眼里。 更像一种“可以忽略的装饰”。
你可以逐步升级工具链。 不需要一口气把全世界重编。
这东西是怎么进标准的
先有编译器扩展。
再有人用宏把它们粘起来。
然后大家受够了。
C++11 先把“属性语法”定下来。
C++14 才把 [[deprecated]] 这种“具体属性”放进来。
它提醒的是“使用点”,不是“定义点”
很多新手会以为。
我在头文件里写了 [[deprecated]]。
那编译就该立刻报一堆。
但它通常提醒的是“谁在用”。 也就是调用那一行。
你可以把它理解成。 责任跟着使用者走。
一个线上味道的场景
我给你一个很小的项目味道。
你写了个解析函数。
旧版为了图省事,返回一个“裸指针”(就是普通 T*,你得自己记得 delete)。
上线后某天晚上。
服务内存开始往上爬。
然后啪一下。
被 OOM 干掉了。
OOM 你可以理解成。 “内存不够用了,被系统强制结束”。
你回头一看。
有人在调用旧接口。 但忘了释放。
复现起来甚至不需要很复杂。
int* parse(const char* s);
void handle(const char* s) {
int* p = parse(s);
// ... 用完就返回了
} // 忘了 delete p;
这里第一行只是“声明”。
它是在告诉别人:有这么个函数。
实现可能在另一个 .cpp 里。
这种 bug 的阴险在于。 你本地跑两下没事。 它要等线上流量一上来,才慢慢把你拖下水。
于是你想做两件事。 给大家一个更安全的新接口。
同时让继续用旧接口的人。 “每次编译都被念一遍”。
std::unique_ptr 就是干这个的。
它还是指针。
但会在离开作用域(花括号)时自动释放。
(想用它,记得 #include <memory>。)
[[deprecated("returning raw pointer is unsafe; use parse2")]]
int* parse(const char* s);
std::unique_ptr<int> parse2(const char* s);
从这刻起。
只要有人继续写 parse(...)。
warning 会直接贴到他的脸上。 连“我没看到文档”都不好意思说。
void handle(const char* s) {
int* p = parse(s); // 这里会出 warning,并把理由打印出来
delete p;
}
你不需要把人抓到会议室里讲一遍。
编译器会替你讲。
不止函数:类型也可以被黄牌
有时候你想废弃的不是函数。
是一个“老数据结构”。 或者一个老的类型别名。
[[deprecated("use NewConfig")]]
struct Config {
int timeout;
};
只要有人继续写 Config c;。
他一样会收到 warning。
[[deprecated("use size_t")]]
using count_t = int;
这种写法对新手很友好。
他不需要理解“为什么 int 不好”。 只要先顺着路标走就行。
再来两个更常见的小例子
你有时想废弃的是一个常量。
比如大家到处用的魔法数字。
[[deprecated("use kMaxSize")]]
constexpr int MAX_SIZE = 1024;
constexpr int kMaxSize = 1024;
这比全项目搜替换靠谱。
你至少能知道还有谁没改。
你也可能想废弃的是一个成员函数。
struct Parser {
[[deprecated("use parse2")]]
int* parse(const char* s);
std::unique_ptr<int> parse2(const char* s);
};
新人看到 warning。
往往就会顺手点进 parse2()。
这比你在群里喊三次有效。
你也可以只废弃某个重载
有时候你不是想废弃整个名字。
你只是想把“危险版本”踢下去。
void log(const char* s);
[[deprecated("use log(const char*)")]]
void log(char* s);
这里老版本的问题很隐蔽。
char* 往往暗示“我可能会修改这段字符串”。
但调用者可能传进来的是只读内存。
你不一定要在文章里讲透。 但你至少能把路标立起来。
enum 也能贴黄牌
有些项目里,会留一堆“历史选项”。 你想让新代码别再选它。
enum class Mode {
Fast,
Safe,
[[deprecated("use Safe")]]
Legacy,
};
有人再写 Mode::Legacy。
他会立刻看到 warning。
让迁移更温柔一点:旧接口做个“壳”
你有时想废弃一个旧函数。 但又不想马上删。
你可以让旧函数继续工作。 只是每次有人用它,都被提醒一下。
int foo2();
[[deprecated("use foo2")]]
int foo() {
return foo2();
}
老代码不会立刻爆炸。
新代码也有一条明确的路。
一句话的结论
deprecated 不是删掉。
是把风险变成可见。
最后留个亮点
我后来觉得。 编译器有点像项目里最靠谱的 code reviewer。
你写十页迁移文档,它可能没人看。 但你让编译器每天都碎碎念“别用这个了”,这句会一直跟着代码跑。