C++避坑指南(十一)

隐式构造

隐式构造指的就是隐式调用构造函数。换句话说,我们不用写出类型名,而是仅仅给出构造参数,编译期就会自动用它来构造对象。举例来说:

class Test {  public:   Test(int a, int b) {} }; void f(const Test &t) { } void Demo() {  f({1, 2}); // 隐式构造Test临时对象,相当于f(Test{a, b}) }

上面例子中,f需要接受的是Test类型的对象,然而我们在调用时仅仅使用了构造参数,并没有指定类型,但编译器会进行隐式构造。

尤其,当构造参数只有 1 个的时候,可以省略大括号:

class Test {  public:   Test(int a) {}   Test(int a, int b) {} }; void f(const Test &t) { } void Demo() {   f(1); // 隐式构造Test{1},单参时可以省略大括号   f({2}); // 隐式构造Test{2}   f({1, 2}); // 隐式构造Test{1, 2} }

这样做的好处显而易见,就是可以让代码简化,尤其是在构造string或者vector的时候更加明显:

void f1(const std::string &str) {} void f2(const std::vector<int> &ve) {} void Demo() {   f1("123"); // 隐式构造std::string{"123"},注意字符串常量是const char *类型   f2({1, 2, 3}); // 隐式构造std::vector,注意这里是initialize_list构造 }

当然,如果遇到函数重载,原类型的优先级大于隐式构造,例如:

class Test { public:   Test(int a) {} }; void f(const Test &t) {   std::cout << 1 << std::endl; } void f(int a) {   std::cout << 2 << std::endl; } void Demo() {   f(5); // 会输出2 }

但如果有多种类型的隐式构造则会报二义性错误:

class Test1 { public:   Test1(int a) {} }; class Test2 { public:   Test2(int a) {} }; void f(const Test1 &t) {   std::cout << 1 << std::endl; } void f(const Test2 &t) {   std::cout << 2 << std::endl; } void Demo() {   f(5); // ERR,二义性错误 }

在返回值场景也支持隐式构造,例如:

struct err_t {   int err_code;   const char *err_msg; }; err_t f() {   return {0, "success"}; // 隐式构造err_t }

但隐式构造有时会让代码含义模糊,导致意义不清晰的问题(尤其是单参的构造函数),例如:

class System {  public:   System(int version); }; void Operate(const System &sys, int cmd) {} void Demo() {   Operate(1, 2); // 意义不明确,不容易让人意识到隐式构造 }

上例中,System表示一个系统,其构造参数是这个系统的版本号。那么这时用版本号的隐式构造就显得很突兀,而且只通过Operate(1, 2)这种调用很难让人想到第一个参数竟然是System类型的。

因此,是否应当隐式构造,取决于隐式构造的场景,例如我们用const char *来构造std::string就很自然,用一组数据来构造一个std::vector也很自然,或者说,代码的阅读者非常直观地能反应出来这里发生了隐式构造,那么这里就适合隐式构造,否则,这里就应当限定必须显式构造。用explicit关键字限定的构造函数不支持隐式构造:

class Test {  public:   explicit Test(int a);   explicit Test(int a, int b);   Test(int *p); }; void f(const Test &t) {} void Demo() {   f(1); // ERR,f不存在int参数重载,Test的隐式构造不允许用(因为有explicit限定),所以匹配失败   f(Test{1}); // OK,显式构造   f({1, 2}); // ERR,同理,f不存在int, int参数重载,Test隐式构造不许用(因为有explicit限定),匹配失败   f(Test{1, 2}); // OK,显式构造   int a;   f(&a); // OK,隐式构造,调用Test(int *)构造函数 }

还有一种情况就是,对于变参的构造函数来说,更要优先考虑要不要加explicit,因为变参包括了单参,并且默认情况下所有类型的构造(模板的所有实例,任意类型、任意个数)都会支持隐式构造,例如:

class Test {  public:   template <typename... Args>   Test(Args&&... args); }; void f(const Test &t) {} void Demo() {   f(1); // 隐式构造Test{1}   f({1, 2}); // 隐式构造Test{1, 2}   f("abc"); // 隐式构造Test{"abc"}   f({0, "abc"}); // 隐式构造Test{0, "abc"} }

所以避免爆炸(生成很多不可控的隐式构造),对于变参构造最好还是加上explicit,如果不加的话一定要慎重考虑其可能实例化的每一种情况。

在谷歌规范中,单参数构造函数必须用explicit限定,但笔者认为这个规范并不完全合理,在个别情况隐式构造意义非常明确的时候,还是应当允许使用隐式构造。另外,即便是多参数的构造函数,如果当隐式构造意义不明确时,同样也应当用explicit来限定。所以还是要视情况而定。

C++支持隐式构造,自然考虑的是一些场景下代码更简洁,但归根结底在于C++主要靠 STL 来扩展功能,而不是语法。举例来说,在 Swift 中,原生语法支持数组、map、字符串等:

let arr = [1, 2, 3] // 数组 let map = [1 : "abc", 25 : "hhh", -1 : "fail"] // map let str = "123abc" // 字符串

因此,它并不需要所谓隐式构造的场景,因为语法本身已经表明了它的类型。

而 C++不同,C++并没有原生支持std::vectorstd::mapstd::string等的语法,这就会让我们在使用这些基础工具的时候很头疼,因此引入隐式构造来简化语法。所以归根结底,C++语言本身考虑的是语法层面的功能,而数据逻辑层面靠 STL 来解决,二者并不耦合。但又希望程序员能够更加方便地使用 STL,因此引入了一些语言层面的功能,但它却像全体类型开放了。

举例来说,Swift 中,[1, 2, 3]的语法强绑定Array类型,[k1:v1, k2,v2]的语法强绑定Map类型,因此这里的“语言”和“工具”是耦合的。但 C++并不和 STL 耦合,他的思路是{x, y, z}就是构造参数,哪种类型都可以用,你交给vector时就是表示数组,你交给map时就是表示 kv 对,并不会将“语法”和“类型”做任何强绑定。因此把隐式构造和explicit都提供出来,交给开发者自行处理是否支持。

这是我们需要体会的 C++设计理念,当然,也可以算是 C++的缺陷。

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/1268

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2022年11月2日 下午12:23
下一篇 2022年11月2日 下午12:25

相关推荐