隐式构造函数转换的编译器优化

Compiler optimization of implicit constructor conversion

本文关键字:编译器 优化 转换 构造函数      更新时间:2023-10-16

在下面的代码中,我希望调用A的构造函数,然后调用A的复制构造函数。然而,事实证明只有构造函数被调用。

// MSVC++ 2008
class A
{
public:
A(int i):m_i(i)
{
cout << "constructorn";
}
A(const A& a)
{
m_i = a.m_i;
cout << "copy constructorn";
}
private:
int m_i;
};
int main()
{
// only A::A() is called
A a = 1;
return 0;
}

我想编译器足够聪明,可以优化掉第二个调用,直接用构造函数初始化对象a。那么,它是标准定义的行为,还是只是实现定义的?

这是标准配置,但不涉及优化

事实上,我相信有一个优化涉及,但它仍然是完全标准的

此代码:

A a = 1;

调用A的转换构造函数††A具有单个转换构造函数A(int i),该构造函数允许从intA隐式转换

如果在构造函数声明之前使用explicit,则会发现代码无法编译。

class A
{
public:
explicit A(int i) : m_i(i) // Note "explicit"
{
cout << "constructorn";
}
A(const A& a)
{
m_i = a.m_i;
cout << "copy constructorn";
}
private:
int m_i;
};
void TakeA(A a)
{
}
int main()
{
A a = 1;     // Doesn't compile
A a(1);      // Does compile
TakeA(1);    // Doesn't compile
TakeA(A(1)); // Does compile
return 0;
}

†再次查看标准后,我最初可能错了。

8.5初始化程序[dcl.init]

12.参数传递、函数中发生的初始化return,抛出异常(15.1),处理异常(15.3),并且大括号包围的初始值设定项列表(8.5.1)被调用复制初始化,相当于形式

T x = a;

14.初始化程序的语义如下。目标类型是正在初始化的对象或引用的类型source类型是初始值设定项表达式的类型。源类型当初始值设定项包含大括号或是带括号的表达式列表。

  • 如果目标类型是(可能是cv限定的)类类型:
    • 如果类是一个聚合(8.5.1),并且初始值设定项是一个包含大括号的列表,请参见8.5.1
    • 如果初始化是直接初始化,或者如果是复制初始化,其中源类型的cv不合格版本与目标类是同一类,或者是目标类的派生类,则考虑构造函数。列举了适用的构造函数(13.3.1.3),并通过重载解析(13.3)选择了最好的构造函数。调用这样选择的构造函数来初始化对象,初始化器表达式作为其参数。如果没有应用构造函数,或者重载解析不明确,则初始化格式不正确
    • 否则(即,对于剩余的副本初始化情况),如13.3.1.4所述,枚举可以从源类型转换为目标类型或(当使用转换函数时)转换为其派生类的用户定义转换序列,并通过过载解析(13.3)选择最佳转换序列。如果转换无法完成或不明确,初始化不正确。调用所选函数时,将初始值设定项表达式作为其参数如果函数是构造函数,则调用初始化目标类型的临时函数。调用的结果(对于构造函数来说是临时的)然后用于根据上面的规则直接初始化作为复制初始化目标的对象。在某些情况下,允许实现通过将中间结果直接构造到正在初始化的对象中来消除这种直接初始化中固有的复制;参见12.2、12.8

。。。

所以从某种意义上说,这在很大程度上是一种优化。但我并不担心它,因为它是标准明确允许的,而且现在几乎每个编译器都会执行省略。

有关初始化的更彻底的处理,请参阅GotW的这篇文章(#36)。文章似乎同意上述对标准的解释:

注意:在最后一种情况下("T t3=u;"),编译器可以调用用户定义的转换(创建临时对象)和T副本构造函数(从临时中构造t3),或者它可以选择删除临时并直接从u构建t3(这将最终等于"T t3(u);")。自1997年7月以来最终的标准草案,编译器可以省略临时对象已被限制,但仍然允许优化和返回值优化。

ISO/IEC 14882:2003 C++标准参考

12.3.1构造函数[class.conv.ctor]的转换

1.在没有函数说明符的情况下声明的构造函数explicit可以用单个参数调用的指定从将其第一个参数的类型设置为其类的类型。这样的构造函数被称为转换构造函数[示例:

class X {
// ...
public:
X(int);
X(const char*, int =0);
};
void f(X arg)
{
X a = 1;        // a = X(1)
X b = "Jessie"; // b = X("Jessie",0)
a = 2;          // a = X(2)
f(3);           // f(X(3))
}

-结束示例]

2.显式构造函数构造对象就像非显式构造函数一样构造函数,但仅当直接初始化语法(8.5)或明确使用强制型铸件(5.2.9、5.4)的情况下。默认值构造函数可以是显式构造函数;这样的构造函数用于执行默认初始化或值初始化(8.5)。[示例:

class Z {
public:
explicit Z();
explicit Z(int);
// ...
};
Z a;                      // OK: default-initialization performed
Z a1 = 1;                 // error: no implicit conversion
Z a3 = Z(1);              // OK: direct initialization syntax used
Z a2(1);                  // OK: direct initialization syntax used
Z* p = new Z(1);          // OK: direct initialization syntax used
Z a4 = (Z)1;              // OK: explicit cast used
Z a5 = static_cast<Z>(1); // OK: explicit cast used

-结束示例]

此处没有优化。当=用于初始化时,它相当于(几乎)用右手边作为参数调用构造函数。所以这个:

A a = 1;

(大部分)相当于:

A a(1);