为什么这里需要一个复制构造函数?

5
考虑下面的代码:

以下是代码:

struct S
{
    S() {}
    void f();
private:
    S(const S&);
};

int main()
{
    bool some_condition;
    S my_other_S;
    (some_condition ? S() : my_other_S).f();
    return 0;
}

gcc编译此代码失败,报错如下:

test.cpp: In function 'int main()':
test.cpp:6:5: error: 'S::S(const S&)' is private
test.cpp:13:29: error: within this context

我不明白为什么在那一行需要进行复制构造 - 目的只是在一个默认构造的S实例或者my_other_S上简单地调用f(),也就是说应该等同于:

if (some_condition)
    S().f();
else
    my_other_S.f();

第一种情况有什么不同之处,为什么需要复制构造函数?

编辑:那么,在表达式上下文中有没有办法表达“在临时对象或预先存在的对象上执行此操作”?


if语句不是做你想要的吗?除了混淆之外,用单个表达式完成所有操作有什么优点? - Bo Persson
@Bo if语句需要重复f()部分,实际上它可能是一个有许多参数的函数,每个参数可能都是一个冗长表达式的结果...你知道怎么回事。 - HighCommander4
此外,在某些情况下,可能无法将表达式重写为if语句 - 例如,如果该表达式用作初始化列表中实例变量的初始化器。 - HighCommander4
@Bo 另一个需要保持表达式上下文的情况的例子是,如果您需要类型推断存储 f() 返回值的变量,即您可以执行 auto x = (some_condition ? S() : my_other_S).f();,但您无法执行 auto x; if (some_condition) x = S().f(); else x = my_other_S.f();。我相信此时还有许多情况我没有想到的。 - HighCommander4
4个回答

9
< p > 如果其中一个参数是 rvalue,则 ?: 的结果是一个 rvalue,即一个新对象。为了创建这个 rvalue,编译器必须复制结果的任何内容。 < /p >
if (some_condition)
    S().f(); // Compiler knows that it's rvalue
else
    my_other_S.f(); // Compiler knows that it's lvalue

这是因为您无法执行相同的操作

struct B { private: B(const B&); };
struct C { C(B&); C(const B&); };
int main() {
    B b;
    C c(some_condition ? b : B());
}

我更改了我的示例,因为旧的示例有点糟糕。您可以清楚地看到这里没有办法编译此表达式,因为编译器无法知道要调用哪个构造函数。当然,在这种情况下,编译器可以将两个参数强制转换为const B&,但由于某些不太相关的原因,它不会这样做。
编辑:不,没有,因为关于它的重要数据(rvalue或lvalue)在运行时变化。编译器试图通过复制构造函数将其转换为rvalue来解决此问题,但它无法复制,因此无法编译。

如果它总是被复制,那么以下内容将无法按预期工作:(test ? varA : varB ) = 5; 这应该将5分配给两个变量中的一个。 - Martin York
@HighCommander4:我不再看到你的任何评论或回答了。 - Puppy
@AndreyT,@DeadMG:显然它并非总是被复制(因为我的示例有效)。 评论的重点是答案在这一点上不是明确的(特别是如果读者不熟悉l / r / x / bla值)。 - Martin York
此外,只要所涉及的函数不需要将 "this" 参数作为右值引用,那么在右值对象或左值对象上调用它有什么问题呢? - HighCommander4
@HighCommander4:因为你没有明确请求,所以为了好玩而开始制作右值引用是极其不安全的。此外,在这个运算符中似乎不想产生引用结果,我真的不太确定为什么。至于rvaluelvalue,很容易构造出标准为左值或右值定义不同行为的情况-例如decltype - Puppy
显示剩余6条评论

8

来自草案n3242的[expr.cond](措辞):

否则,如果第二个和第三个操作数具有不同的类型,并且其中一个具有(可能带有cv限定的)类类型,或者如果两个操作数都是相同值类别和类型的glvalue,除了cv限定之外,则尝试将这些操作数中的每个转换为另一个的类型。确定类型匹配的操作数表达式E1是否可以转换为类型E2的过程定义如下:

  • 如果E2是lvalue:E1可以被转换以匹配E2,如果E1可以隐式转换(第4条)为类型“T2的lvalue引用”,在转换中的约束条件是引用必须直接绑定(8.5.3)到lvalue。
  • 如果E2是xvalue:E1可以被转换以匹配E2,如果E1可以隐式转换为类型“T2的rvalue引用”,在转换中的约束条件是引用必须直接绑定。
  • 如果E2是rvalue或者以上面的任何一种转换都不能完成,并且至少有一个操作数具有(可能带有cv限定的)类类型:

    • 如果E1E2具有类类型,并且底层类类型相同或者其中一个是另一个的基类:E1可以被转换以匹配E2,如果T2的类与T1的类相同或是其基类,并且T2的cv限定与T1的cv限定相同或更高。如果应用了转换,则通过从E1复制初始化类型为T2的临时对象并使用该临时对象作为转换后的操作数将E1更改为T2的prvalue。

这条规则提到了复制初始化,但不适用,因为两个操作数具有相同的类型

如果第二个和第三个操作数是相同值类别的glvalue并且具有相同的类型,则结果是该类型和值类别,并且如果第二个或第三个操作数是位域,或者两者都是位域,则它是位域。

此规则不适用,因为S()是rvalue,而my_other_S是lvalue。

否则,结果是一个prvalue。如果第二个和第三个操作数的类型不相同,并且其中一个具有(可能带限定符的)类类型,则使用重载决议来确定要应用于操作数的转换(如果有的话)(13.3.1.2、13.6)。如果重载决议失败,则程序是非法的。否则,因此确定的转换被应用,并且将转换后的操作数用于本节的其余部分。 对于第二个和第三个操作数执行左值到右值(4.1)、数组到指针(4.2)和函数到指针(4.3)标准转换。在这些转换之后,以下情况之一必须成立: 第二个和第三个操作数具有相同的类型;结果是该类型。如果操作数具有类类型,则结果是该类型的prvalue临时对象,该对象从第二个操作数或第三个操作数中的一个进行复制初始化,具体取决于第一个操作数的值。
应用此规则时,结果进行了复制初始化(我强调)。

4

这是一个旧问题。请参见此处

http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#446

根据委员会的决定,在您的示例中,?:运算符应始终返回一个临时对象,这意味着对于my_other_s分支,原始的my_other_s对象将被复制。这就是编译器需要复制构造函数的原因。

这种语言还没有出现在C++03中,但许多编译器从一开始就实现了这种方法。


1
关于您更新的问题,如果允许修改S的定义,则以下解决方法可能有所帮助:
struct S
{
    ...
    S& ref() { return *this; } // additional member function
    ...
};

(some_condition ? S().ref() : my_other_S).f();

希望这能有所帮助。

是的!这正是我原始代码的意图。但不幸的是,我必须编写和调用一个特殊的成员函数来实现它... - HighCommander4

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接