【问题标题】:Is it possible to initialize member variable (or base class) of a non-copyable type?是否可以初始化不可复制类型的成员变量(或基类)?
【发布时间】:2020-07-15 18:20:42
【问题描述】:

考虑following code

struct S
{
    S() = default;
    S(S const&) = delete;
    // S(S&&) = delete;  // <--- uncomment for a mind-blowing effect:
                         // MSVC starts compiling EVERY case O_O
};

S foo() { return {}; }

struct X : S
{
//    X() : S(foo()) {}   // <----- all compilers fail here
};

struct Y
{
    S s;
    Y() : s(foo()) {}   // <----- only MSVC fails here
};

struct Z
{
    S s = {};           // ... and yet this is fine with every compiler
    Z() {}
};

//S s1(foo());      // <-- only MSVC fails here
//S s2 = foo();     // <-- only MSVC fails here

问题:

  • 看起来没有办法用纯右值初始化不可复制的基类——这是正确的吗?看起来标准有缺陷(或者我尝试的所有编译器都不兼容)

  • MSVC 无法初始化成员变量——这是否意味着它不兼容?有没有办法解决这个问题?

  • 为什么添加 S(S&amp;&amp;) = delete; 会导致 MSVC 编译每个案例?

【问题讨论】:

  • S 可以移动吗?如果你添加S(S&amp;&amp;) = default;,这两种底部的情况都应该编译。
  • @CoryKramer 好吧,如果 S 是可移动的,它显然会起作用。 :) 但是你把我推到另一个发现——明确删除 mctor 会导致 MSVC 编译每个案例。 O_O
  • 您使用的是什么版本的 MSVC?你指定了什么语言标志?
  • @NathanOliver 检查提供的上帝螺栓链接(最新可用,c++latest
  • 你有没有试过X是否调用S拷贝构造函数,当它存在的时候?

标签: c++ c++17 language-lawyer


【解决方案1】:

所以,我想我找到了标准的相关部分,我认为编译器对X 有误。 (所有链接都指向标准草案,所以在 C++17 中可能会有所不同,我稍后会检查。但 gcc10 和 clang10 也因-std=c++20 而失败,所以这并不重要)。

关于基类的初始化(重点是我的):class.base.init/7

mem-initializer 中的表达式列表或花括号初始化列表用于根据 的初始化规则初始化指定的子对象(或者,在委托构造函数的情况下,完整的类对象) [dcl.init] 用于直接初始化

我认为这告诉我们,X() : S(foo()) {} 不应该与S s = foo() 不同,但让我们看看dcl.init/17.6.1

如果初始化表达式是纯右值并且源类型的 cv 非限定版本与目标类是同一类,则初始化表达式用于初始化目标对象。 [示例:T x = T(T(T())); 调用T 默认构造函数来初始化x。 —结束示例]

这对我来说意味着X() : S(foo()) {} 应该调用默认构造函数。我还测试了(完全符合示例)X() : S(S()) {},这在 clang 和 g++ 上也失败了。所以在我看来,编译器有缺陷。

【讨论】:

  • Nit: S s = foo() 不是直接初始化。由于=,它是复制初始化。不过没关系;实际直接初始化S s(foo()) 也可以。
  • 这是标准中的一个缺陷。所谓的“保证省略”不能对可能重叠的子对象起作用。
  • @T.C.知道是否正在解决此缺陷吗?在这种情况下,我们是否会在未来的某个 C++ 版本中保证省略?
【解决方案2】:

看起来没有办法用纯右值初始化不可复制的基类——这是正确的吗?看起来标准有缺陷(或者我尝试的所有编译器都不兼容)

标准说它应该有效。标准错了。

基类子对象(更一般地说,可能重叠的子对象)可能具有与相同类型的完整对象不同的布局,或者其填充可能被其他对象重用。因此,不可能从返回纯右值的函数中删除副本或移动,因为该函数不知道它没有初始化一个完整的对象。

其余的是 MSVC 错误。

【讨论】:

  • 你能扩展一下“可能重叠的子对象”吗?它是什么以及为什么它可能有不同的布局?我很难相信,仅仅是因为当我将T* 传递给另一个函数时——无论指针指向子对象还是完整对象,该函数总是期望相同的布局。嗯……可能和 vtables 有关系?
  • 虚拟基地。不过,填充重用更为常见——完整对象的初始化可以随意践踏填充字节;如果对象实际上是基类子对象,则这些字节可能被其他对象占用。
  • 标准没有错;所有这些都取决于编译器实现来enforce。布局,重叠,所有这些都在编译器的控制之下。如果编译器希望以保证省略会覆盖已经初始化的其他一些子对象的方式进行填充重用......那么编译器不能与该对象重叠。标准不要求重叠;这只是一种优化,如果它会破坏东西,编译器必须避免。
  • 请注意,如果经历保证省略的对象被初始化初始化任何重叠到其填充的对象之前,这很好。因此,EBO 重叠工作正常,因为初始化的顺序是明确的,并且空类与非空类型的重叠不会导致乱序初始化问题。此外,完全空的类型不必关心,因为它们实际上并没有对它们的“存储”做任何事情。
  • @NicolBolas 考虑到该论文声称正在将有关复制省略的现有实践形式化,“错误”是一个很好的描述,“在不破坏当前 ABI 的情况下无法实现”同样好。完全空的类型 do 必须注意,因为当值初始化执行零初始化时,需要将填充清零,这可以通过检查对象表示来观察;该填充可能与已经初始化的基类对象重叠; [[no_unique_address]] 成员同样有问题。 (这被跟踪为核心问题 2403。)
【解决方案3】:

不,不允许,但自从c++17 以来,有一些新功能,其中一个功能不再copy object

函数返回 prvalues 不再复制对象 (mandatory copy elision),并且有一个新的prvalue-to-glvalue 转换称为 临时物化转换。此更改意味着 复制 elision 现在是保证,并且甚至适用于不是的类型 可复制或可移动。这允许您定义返回的函数 这样的类型。

Guaranteed copy elision C++17


以下函数,从不返回S() 对象并返回std::initialization_list{},而是作为S s ={},是一个有效 转换,所以基于复制省略优化,它不会返回一个副本粗略地说 - 直接返回@987654329 @ 本身。注意临时实现转换

S foo() { return {}; }

以下不起作用,

S foo() { S s = {}; return s; }

因此,Y() : s(foo()) {} 这一行,粗略地说 - 现在可以解释为隐式类型转换

S s = {}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-02-20
    • 1970-01-01
    • 2020-01-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多