在C++11中,使用匿名类构造一个对象时会发生什么?

新手上路,请多包涵

我遇到了一个关于C++中类构造的问题。我定义了一个类obj,这个类中有两个成员和各种构造函数,其中包括默认构造函数、拷贝构造函数和移动构造函数。每个构造函数的函数体中都会输出一个字符串,表示这个构造函数被执行了。下面是类的定义:

class obj{
public:
    string *e;
    string *n;
    obj():e(new string("123")),n(new string("456")){    //默认构造
        cout<<"default"<<endl;
    }
    obj(string *s1, string *s2):e(s1),n(s2){    //非默认构造
        cout<<"non default"<<endl;
    }
    obj(obj *t):e(t->e), n(t->n){   //用指针构造
        cout<<"pointer"<<endl;
    };
    obj(obj& t):e(t.e), n(t.n){   //拷贝构造
        cout<<"copy"<<endl;
    };
    obj(const obj& t):e(t.e), n(t.n){   //拷贝构造
        cout<<"const copy"<<endl;
    };
    obj(obj &&t) noexcept :e(t.e), n(t.n) {    //移动构造
        cout<<"move"<<endl;;
    }
    obj(const obj &&t) noexcept :e(t.e), n(t.n) {    //移动构造
        cout<<"const move"<<endl;;
    }
    ~obj(){
        delete e;
        delete n;
    }
};

然后我在主函数中构造了一个名为o3obj对象,构造参数是一个匿名的obj对象,我想看看构造o3时会调用哪个构造函数。代码如下:

int main(){

    obj o3(obj(new string("qqq"), new string("zzz"))); //这属于什么构造???
    cout<<*o3.e<<endl;
    return 0;
}

为了防止编译器优化,我给g++指定了-o0选项。最后执行结果如下:

non default
qqq

根据程序的执行结果来看,匿名类构造时调用了类的第二个构造函数,而o3没有调用任何一个构造函数。但是程序最终打印出了o3的成员,说明o3被成功构造了。这令我百思不得其解,o3是如何被构造出来的,调用了哪个构造函数?

程序是用gcc 7.4.0编译的,在ubuntu下执行。

阅读 3.5k
2 个回答

Copy elision

====================

gcc 7.4.0 默认的标注是 gnu++14 ,基础标准是 c++14 。

C++ 14 里(C++14 draft n4140),定义了在若干情况下,编译器(作为一项优化)可以选择不调用(本应该被调用的)拷贝/移动构造函数,称作 copy elision 。

其中一种情况是:

12.8 Copying and moving class objects
32 When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization. This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):
......

  • when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move

......

这正是你现在的情况,一个临时对象被被拷贝/移动至一个同类型的变量。此时可以直接在目标对象中构造“临时”对象。

copy elision 可能是唯一一个标准特许的可能改变程序行为的优化。

copy elision 是可选的,gcc 可以使用 -fno-elide-constructors 阻止这一优化。加上之后,可以看到调用了 move constructor 。

也因为其实可选的,即使发生 copy elision ,obj 类仍然需要一个可访问的 move constructor 。如果你把 move constructor 改成 private ,编译将不通过。

============================

C++17 (C++17 draft n4659)以后,情况发生了变化。 prvalue 跟 temporary object 的定义发生了一些变化。

obj(.....) 是一个 prvalue ,prvalue 不再是一个对象。

6.10 Lvalues and ravalues

  • A prvalue is an expression whose evaluation initializes an object or a bit-field, or computes the value of the operand of an operator, as specified by the context in which it appears.

prvalue 在必要的时候,可以生成一个临时对象,叫做 materialize 。

但是,在你的这个程序里,并不需要这个 prvalue 生成临时对象。

关于初始化,有如下规定:

11.6 Initializers
17 The semantics of initializers are as follows. The destination type is the type of the object or reference being initialized and the source type is the type of the initializer expression. If the initializer is not a single (possibly parenthesized) expression, the source type is not defined.
......

  • If the destination type is a (possibly cv-qualified) class type:

    • If the initializer expression is a prvalue and the cv-unqualified version of the source type is the same class as the class of the destination, the initializer expression is used to initialize the destination object. [ Example: T x = T(T(T())); calls the T default constructor to initialize x. — end example ]

用 prvalue 初始化同类型的对象的时候,不会发生 copy/move ,而是直接构造目标对象。

所以,在 C++17 里(可以用 -std=c++17 启用),即使加上 -fno-elide-constructors 也不会调用 copy/move constructor ,因为这已经不是一个编译器优化,而是语言本身在这里不需要 copy/move 。同时,即使将所有 copy/move constructor 全变成 private ,这个程序也可以正确通过编译,因为这里不需要它们。

注:C++17 中同样存在 copy elision ,但是已经不再包含上述情形。

  1. 静止编译器优化构造函数编译选项是:-fno-elide-constructors
  2. 上面的移动构造函数是错误的,必须保证移动后的对象正常析构,上面移动后的原对象和新对象指向同一空间,导致同一指针析构两次
  3. 对于 obj o3(obj(new string("qqq"), new string("zzz"))); 括号里的 obj(new string("qqq"), new string("zzz")) 将调用非默认构造函数,产生的结果是一个右值,所以将调用移动构造函数
  4. 我觉得,对于拷贝构造函数写一个就可以了,没必要写两个,移动构造函数同理。
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题