const 正确性、API 设计与现代属性 时间:2026/05/08
关键词:const、引用、值传递、explicit、[[nodiscard]]、noexcept、enum class、API 边界 核心目标:把“函数签名”写成清楚的契约,让调用者一眼知道所有权、可变性、失败方式和使用方式。
1. API 设计先看函数签名 函数签名不是只给编译器看的,它也是给人看的。
一个好的签名应该回答:
参数会不会被修改
参数会不会被保存
返回值是否必须检查
函数是否可能抛异常
构造是否允许隐式转换
调用者是否转移所有权
对比两个接口:
1 void parse (char * data, int len) ;
和:
1 [[nodiscard]] Config parse_config (std::string_view text) ;
第二个签名更清楚:
输入只是观察,不拥有
输入不会被修改
返回值值得检查
解析结果是一个明确对象
2. const 参数:表达“我不会改它” 读大对象时,优先用 const T&:
1 2 3 4 5 6 7 8 struct User { std::string name; std::string email; }; void print_user (const User& user) { std::cout << user.name << " <" << user.email << ">\n" ; }
如果写成值传递:
1 void print_user (User user) ;
会拷贝整个对象,通常没有必要。
如果参数很小,值传递更简单:
1 2 void set_retry_count (int n) ;void set_timeout (std::chrono::milliseconds timeout) ;
经验:
小标量:值传递
大对象只读:const T&
可空观察:const T*
连续数组观察:std::span<const T>
字符串观察:std::string_view
3. const 成员函数:承诺不改对象状态 成员函数后面的 const 表示不会修改这个对象的可观察状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Cache {public : std::size_t size () const { return items_.size (); } bool empty () const { return items_.empty (); } private : std::vector<int > items_; };
如果一个查询函数没写 const:
那么 const Cache& 就不能调用它。 这会让 API 很难组合。
4. 什么时候用 mutable mutable 用于“逻辑上不改变对象,但需要更新内部缓存”的场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Text {public : explicit Text (std::string s) : data_(std::move(s)) { } std::size_t word_count () const { if (!cached_) { cached_words_ = count_words (data_); cached_ = true ; } return cached_words_; } private : static std::size_t count_words (std::string_view text) ; std::string data_; mutable bool cached_ = false ; mutable std::size_t cached_words_ = 0 ; };
注意:
mutable 不是绕过 const 的万能钥匙
多线程读同一个对象时,mutable 缓存也需要同步
如果缓存逻辑复杂,优先考虑显式构建缓存对象
5. 字符串参数优先考虑 std::string_view 只读、不保存字符串时:
1 2 3 void log_message (std::string_view msg) { std::cout << msg << "\n" ; }
它可以接收:
1 2 3 4 5 6 7 log_message ("hello" );std::string s = "world" ; log_message (s);std::string_view v = "cpp" ; log_message (v);
但不要保存 string_view 指向临时对象:
1 2 3 4 5 6 7 8 9 class Bad {public : void set_name (std::string_view name) { name_ = name; } private : std::string_view name_; };
如果对象要保存字符串,应该拥有它:
1 2 3 4 5 6 7 8 9 class User {public : void set_name (std::string name) { name_ = std::move (name); } private : std::string name_; };
6. 连续数组参数优先考虑 std::span 传统接口:
1 double average (const double * data, std::size_t n) ;
现代写法:
1 2 3 4 5 6 7 8 9 10 11 #include <span> #include <numeric> double average (std::span<const double > xs) { if (xs.empty ()) { return 0.0 ; } double sum = std::accumulate (xs.begin (), xs.end (), 0.0 ); return sum / static_cast <double >(xs.size ()); }
可以接收:
1 2 3 4 5 6 7 std::vector<double > v{1.0 , 2.0 , 3.0 }; std::array<double , 3> a{1.0 , 2.0 , 3.0 }; double raw[] = {1.0 , 2.0 , 3.0 };average (v);average (a);average (raw);
span 只观察,不拥有。 不要让 span 活得比底层数组更久。
7. 值传递 + move:接收要保存的对象 如果函数要把参数保存到成员里,经常可以用值传递:
1 2 3 4 5 6 7 8 9 10 11 12 class Person {public : explicit Person (std::string name) : name_(std::move(name)) { } void rename (std::string name) { name_ = std::move (name); } private : std::string name_; };
调用者传左值时会拷贝一次:
1 2 std::string n = "Alice" ; Person p (n) ;
调用者传右值时通常很高效:
1 2 Person p ("Alice" ) ;Person q (std::string("Bob" )) ;
这比同时写 const std::string& 和 std::string&& 两套重载更简单。
8. explicit:阻止意外隐式转换 单参数构造函数默认可能触发隐式转换:
1 2 3 4 5 6 7 8 9 10 11 class Port {public : Port (int value) : value_ (value) {} private : int value_; }; void connect (Port port) ;connect (80 );
很多时候这不是你想要的。 更推荐:
1 2 3 4 5 6 7 8 9 class Port {public : explicit Port (int value) : value_(value) { } private : int value_; }; connect (Port{80 });
经验:
除非你明确希望它能隐式转换,否则单参数构造函数优先写 explicit。
9. [[nodiscard]]:返回值不能随手丢 错误处理或资源构造结果经常不能忽略:
1 2 3 4 5 6 7 enum class Error { none, file_not_found, permission_denied, }; [[nodiscard]] Error save_config (std::string_view path) ;
调用者如果丢掉返回值,编译器会提醒:
1 save_config ("app.toml" );
更清晰的写法:
1 2 3 if (auto err = save_config ("app.toml" ); err != Error::none) { report (err); }
也可以标记类型:
1 2 3 4 5 6 struct [[nodiscard]] Result { bool ok; std::string message; }; Result load_user (int id) ;
适合 [[nodiscard]] 的返回值:
错误码
std::optional
std::expected
资源句柄
需要调用者继续使用的 builder 结果
10. noexcept:承诺不抛异常 noexcept 不只是优化提示,也是契约。
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <cstddef> #include <utility> class Buffer {public : Buffer (Buffer&& other) noexcept : data_ (std::exchange (other.data_, nullptr )), size_ (std::exchange (other.size_, 0 )) {} private : int * data_ = nullptr ; std::size_t size_ = 0 ; };
移动构造如果是 noexcept,容器扩容时更愿意移动元素而不是拷贝元素。
不要乱写:
1 2 3 void f () noexcept { may_throw (); }
经验:
析构函数默认应不抛
移动构造/移动赋值能保证不抛时写 noexcept
低层工具函数如果不抛,可以写 noexcept
不能保证时不要硬写
11. enum class:避免枚举污染和隐式转换 老式 enum:
1 2 3 4 5 6 enum Color { red, green, }; int x = red;
更推荐:
1 2 3 4 5 6 7 8 enum class Color { red, green, }; void paint (Color c) ;paint (Color::red);
如果要指定底层类型:
1 2 3 4 enum class HttpStatus : int { ok = 200 , not_found = 404 , };
enum class 的好处:
名称不污染外层作用域
不会随便隐式转整数
API 可读性更强
12. 现代属性的几个实用场景 12.1 [[maybe_unused]] 用于故意未使用的变量或参数:
1 2 3 4 5 void on_debug_event ([[maybe_unused]] int code) {#ifdef DEBUG std::cout << code << "\n" ; #endif }
12.2 [[deprecated]] 给旧接口留迁移提示:
1 2 [[deprecated ("use parse_config_v2 instead" )]] Config parse_config (std::string_view text) ;
12.3 [[fallthrough]] 明确 switch 穿透是有意的:
1 2 3 4 5 6 7 8 9 10 switch (level) {case 3 : enable_verbose (); [[fallthrough]]; case 2 : enable_info (); break ; default : break ; }
这些属性不是装饰,它们让代码意图对编译器和读者都更清楚。
13. 返回值设计:值、引用、指针怎么选 13.1 返回值 最常见、最安全:
1 std::vector<int > make_ids () ;
现代 C++ 有移动语义和返回值优化,不要过早改成输出参数。
13.2 返回引用 表示返回对象内部已有内容:
1 2 3 4 5 6 7 8 9 class User {public : const std::string& name () const noexcept { return name_; } private : std::string name_; };
注意调用者不能让引用超过对象生命周期。
13.3 返回指针 适合表达“可能没有”且不转移所有权:
1 const User* find_user (int id) const ;
如果没有值,也可以用:
1 std::optional<User> find_user (int id) const ;
如果对象很大、不想复制,并且不拥有,就用指针或引用包装语义说清楚。
14. 一个完整的小 API 示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include <optional> #include <span> #include <string> #include <string_view> #include <vector> enum class Role { admin, guest, }; struct User { int id = 0 ; std::string name; Role role = Role::guest; }; class UserStore {public : explicit UserStore (std::vector<User> users) : users_(std::move(users)) { } [[nodiscard]] const User* find (int id) const noexcept { for (const auto & user : users_) { if (user.id == id) { return &user; } } return nullptr ; } [[nodiscard]] std::vector<User> find_by_name (std::string_view name) const { std::vector<User> out; for (const auto & user : users_) { if (user.name == name) { out.push_back (user); } } return out; } void append (User user) { users_.push_back (std::move (user)); } void append_all (std::span<const User> users) { users_.insert (users_.end (), users.begin (), users.end ()); } private : std::vector<User> users_; };
这个例子里:
构造函数 explicit
只读查询是 const
可能没有结果时返回指针
必须关注的查询结果加 [[nodiscard]]
保存参数时用值传递 + move
批量只读输入用 span<const User>
15. 常见误区 15.1 所有参数都写 const T& 小整数、枚举、轻量句柄直接值传递更好。
15.2 到处返回 const T& 如果返回的是临时对象或局部变量引用,会立刻悬空。 现代 C++ 返回值通常很便宜,别怕返回对象。
15.3 string_view 当成字符串成员保存 除非你非常清楚底层字符串生命周期,否则成员里保存 std::string。
15.4 所有函数都加 noexcept noexcept 是承诺,不是祝福。 承诺错了会直接终止程序。
15.5 不写 explicit 隐式转换引起的问题很隐蔽。 构造函数默认倾向 explicit 是很好的工程习惯。
16. 一页总结 现代 C++ API 设计最常用的几条规则:
只读大对象用 const T&
字符串观察用 std::string_view
连续数组观察用 std::span
要保存的对象可以值传递再 move
查询成员函数尽量写 const
单参数构造函数优先 explicit
重要返回值加 [[nodiscard]]
能保证不抛时才写 noexcept
一句话:
好 API 的核心是把所有权、可变性和失败语义写在签名里,而不是藏在文档里。