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

发布于 2024-11-28 16:25:32 字数 464 浏览 1 评论 0原文

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

// MSVC++ 2008
class A
{
public:
   A(int i):m_i(i)
   {
      cout << "constructor\n";
   }
   A(const A& a)
   {
      m_i = a.m_i;
      cout << "copy constructor\n";
   }

private:
   int m_i;
};

int main()
{
   // only A::A() is called
   A a = 1;
   return 0;
}

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

In the following code, I expect A's constructor is called, followed by A's copy constructor. However, It turns out only constructor is get called.

// MSVC++ 2008
class A
{
public:
   A(int i):m_i(i)
   {
      cout << "constructor\n";
   }
   A(const A& a)
   {
      m_i = a.m_i;
      cout << "copy constructor\n";
   }

private:
   int m_i;
};

int main()
{
   // only A::A() is called
   A a = 1;
   return 0;
}

I guess the compiler is smart enough to optimize away the second call, to initialize the object a directly with the constructor. So is it a standard-defined behavior or just implementation-defined?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

悲歌长辞 2024-12-05 16:25:32

这是标准的,但不涉及优化。

实际上,我相信涉及优化,但它仍然完全标准。

此代码:

A a = 1;

调用 转换 A 的构造函数††A 有一个转换构造函数 A(int i),允许从 int隐式转换一个。

如果您在构造函数声明前加上 explicit,您会发现代码无法编译。

class A
{
public:
    explicit A(int i) : m_i(i) // Note "explicit"
    {
        cout << "constructor\n";
    }

    A(const A& a)
    {
        m_i = a.m_i;
        cout << "copy constructor\n";
    }

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. 参数传递、函数中发生的初始化
返回,抛出异常(15.1),处理异常(15.3),
并调用大括号括起来的初始值设定项列表 (8.5.1)
复制初始化并且等同于形式

<前><代码> T x = a;

14.初始化器的语义如下。 目的地
type 是正在初始化的对象或引用的类型
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. 未使用函数说明符声明的构造函数显式
可以使用单个参数调用,指定从
它的第一个参数的类型为其类的类型。这样一个
构造函数称为转换构造函数。 [示例:

<前><代码> 类 X {
// ...
民众:
X(整数);
X(const char*, int =0);
};

无效 f(X 参数)
{
X a = 1; // a = X(1)
X b =“杰西”; // b = X("杰西",0)
a = 2; // a = X(2)
f(3); // f(X(3))
}

—结束示例]

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

 类 Z {
     民众:
         显式 Z();
         显式 Z(int);
         // ...
     };

     Z a; // OK:已执行默认初始化
     Z a1 = 1; // 错误:没有隐式转换
     Z a3 = Z(1); // OK:使用直接初始化语法
     Z a2(1); // OK:使用直接初始化语法
     Z* p = 新 Z(1); // OK:使用直接初始化语法
     Z a4 = (Z)1; // OK:使用显式强制转换
     Z a5 = static_cast(1); // OK:使用显式强制转换

—结束示例]

It's standard, but there's no optimization involved.

Actually, I believe there is an optimization involved, but it's still entirely standard.

This code:

A a = 1;

invokes the converting constructor†† of A. A has a single converting constructor A(int i) that allows an implicit conversion from int to A.

If you prepend the constructor declaration with explicit, you'll find the code won't compile.

class A
{
public:
    explicit A(int i) : m_i(i) // Note "explicit"
    {
        cout << "constructor\n";
    }

    A(const A& a)
    {
        m_i = a.m_i;
        cout << "copy constructor\n";
    }

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;
}

† After looking at the standard again, I may have been initially wrong.

8.5 Initializers [dcl.init]

12. The initialization that occurs in argument passing, function
return, throwing an exception (15.1), handling an exception (15.3),
and brace-enclosed initializer lists (8.5.1) is called
copy-initialization and is equivalent to the form

   T x = a;

14. 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. The source type
is not defined when the initializer is brace-enclosed or when it is a
parenthesized list of expressions.

...

  • If the destination type is a (possibly cv-qualified) class type:
    • If the class is an aggregate (8.5.1), and the initializer is a brace-enclosed list, see 8.5.1.
    • If the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated (13.3.1.3), and the best one is chosen through overload resolution (13.3). The constructor so selected is called to initialize the object, with the initializer expression(s) as its argument(s). If no constructor applies, or the overload resolution is ambiguous, the initialization is ill-formed.
    • Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in 13.3.1.4, and the best one is chosen through overload resolution (13.3). If the conversion cannot be done or is ambiguous, the initialization is ill-formed. The function selected is called with the initializer expression as its argument; if the function is a constructor, the call initializes a temporary of the destination type. The result of the call (which is the temporary for the constructor case) is then used to direct-initialize, according to the rules above, the object that is the destination of the copy-initialization. In certain cases, an implementation is permitted to eliminate the copying inherent in this direct-initialization by constructing the intermediate result directly into the object being initialized; see 12.2, 12.8.

...

So in one sense it is very much an optimization. But I wouldn't worry about it since it is explicitly allowed by the standard and just about every compiler nowadays does the elison.

For a much more thorough treatment on initialization, see this GotW article (#36). The article seems to agree with the above interpretation of the standard:

NOTE: In the last case ("T t3 = u;") the compiler could call both the
user-defined conversion (to create a temporary object) and the T copy
constructor (to construct t3 from the temporary), or it could choose
to elide the temporary and construct t3 directly from u (which would
end up being equivalent to "T t3(u);"). Since July 1997 and in the
final draft standard, the compiler's latitude to elide temporary
objects has been restricted, but it is still allowed for this
optimization and for the return value optimization.

††

ISO/IEC 14882:2003 C++ Standard reference

12.3.1 Conversion by constructor [class.conv.ctor]

1. A constructor declared without the function-specifier explicit
that can be called with a single parameter specifies a conversion from
the type of its first parameter to the type of its class. Such a
constructor is called a converting constructor. [Example:

     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))
     }

—end example]

2. An explicit constructor constructs objects just like non-explicit
constructors, but does so only where the direct-initialization syntax
(8.5) or where casts (5.2.9, 5.4) are explicitly used. A default
constructor may be an explicit constructor; such a constructor will be
used to perform default-initialization or valueinitialization (8.5).
[Example:

     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

—end example]

浴红衣 2024-12-05 16:25:32

这里没有优化。当在初始化中使用 = 时,它相当于(几乎)以右侧作为参数调用构造函数。所以这:(

A a = 1;

大部分)相当于:

A a(1);

No optimization here. When = is used in the initialization, it is eqivalent(nearly) to calling the constructor with the right hand side as an argument. So this:

A a = 1;

Is (mostly) equivalent to this:

A a(1);
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文