explicit 控制某个构造函数或转换函数能否参与隐式转换。它不取消构造能力,也不表示这个类从此不能复制。读调用处时,要同时看选中了哪个函数,以及采用哪一种初始化语法。
本篇以 C++17 为准,保留 MyClass 与数值 42 的原场景,并把成功程序和预期编译错误分开。
构造函数:显式写出转换意图
跳转到“构造函数:显式写出转换意图”假设声明为 explicit MyClass(int),以下结果成立:
| 写法 | 结果 | 原因 |
|---|---|---|
MyClass a(42); | 可以 | 直接初始化 |
MyClass b{42}; | 可以 | 直接列表初始化 |
MyClass c = 42; | 不可以 | 拷贝初始化不能用这个显式转换构造函数 |
MyClass d = {42}; | 不可以 | 拷贝列表初始化选中显式构造函数时不合法 |
printMyClass(42); | 不可以 | 实参需要隐式转换为 MyClass |
printMyClass(MyClass{42}); | 可以 | 实参中已明确构造了对象 |
MyClass e = a; | 通常可以 | 这里使用可用的拷贝构造函数,不是 MyClass(int) |
非 explicit 的单参数构造函数、或其他参数都有默认值的构造函数,常产生自动转换。explicit 也可以用于多参数构造函数和默认构造函数,并非只允许放在单参数构造函数前。多参数形式同样会影响拷贝列表初始化。C++17 转换构造函数
“拷贝初始化”是语法和语义分类,不代表一定真的调用拷贝构造函数,也不只发生在等号旁。例如按值传参和返回值也涉及相应的初始化规则。
转换函数:从类对象转到别的类型
跳转到“转换函数:从类对象转到别的类型”explicit operator int() const 允许 static_cast<int>(object) 等显式用法,但不能让 printInt(object) 或 int n = object; 自动成功。转换函数不另写返回类型;目标类型已经在 operator int 中。C++17 转换函数
explicit operator bool() 有一个常用边界:if (object)、while (object) 和逻辑运算需要的布尔条件可以使用它;普通的 bool b = object; 仍被拒绝。布尔条件上下文有自己的转换规则,不能把这个结论推广成显式 operator int() 也能隐式传给整数参数。条件中的布尔转换
完整程序:自动转换、明确构造与条件判断
跳转到“完整程序:自动转换、明确构造与条件判断”#include <cassert>#include <ostream>#include <sstream>#include <string>#include <type_traits>
class ImplicitValue {public: ImplicitValue(int value) : value_(value) {} int value() const { return value_; }private: int value_;};
class MyClass {public: explicit MyClass(int value) : value_(value) {} void print(std::ostream& out) const { out << value_; } explicit operator int() const { return value_; }private: int value_;};
class Ready {public: explicit Ready(bool value) : value_(value) {} explicit operator bool() const { return value_; }private: bool value_;};
int readImplicit(const ImplicitValue& value) { return value.value(); }void printMyClass(const MyClass& object, std::ostream& out) { object.print(out); }void printInt(int value, std::ostream& out) { out << value; }
int main() { static_assert(std::is_convertible_v<int, ImplicitValue>); static_assert(!std::is_convertible_v<int, MyClass>); static_assert(std::is_constructible_v<MyClass, int>); static_assert(std::is_copy_constructible_v<MyClass>); static_assert(!std::is_convertible_v<MyClass, int>); static_assert(!std::is_convertible_v<Ready, bool>);
assert(readImplicit(42) == 42); MyClass first(42); MyClass second{42}; MyClass copied = first; assert(static_cast<int>(second) == 42); assert(static_cast<int>(copied) == 42);
std::ostringstream out; printMyClass(MyClass{42}, out); out << ' '; printInt(static_cast<int>(first), out); assert(out.str() == "42 42");
const Ready ready{true}; bool entered = false; if (ready) entered = true; assert(entered && ready && !Ready{false}); const bool direct(ready); assert(direct);}在上面的类定义下,单独尝试 MyClass bad = 42;、MyClass bad = {42};、printMyClass(42, out);、printInt(first, out); 和 bool bad = ready;,都应产生编译错误。它们是接口约束的验证,不应直接混入声称可以运行的程序中。
设计接口时,先决定某种转换是否应该自动发生。例如“整数是计数、容量还是设备编号”可能需要读者明确表达;已有自然值转换语义的类型则可有意识地允许转换。explicit 提供这个选择,本身不验证数值范围或业务正确性。