C++11 中的默认复制赋值运算符何时使用按位复制而不是按成员复制?

Zih*_*Liu 1 c++ c++11

在 x86-64 GCC 13.1 和 Clang 16.0.0 中,该copy<PrivateBase>函数使用成员方式复制,而该copy<PublicBase>函数使用按位复制。您可以参考编译器资源管理器上的详细源代码和汇编代码,或者查看下面提供的代码片段:

class PublicBase {
public:
    int num;
    char c1;
};

class PrivateBase {
private:
    int num;
    char c1;
};


template<typename T>
__attribute_noinline__ void copy(T *dst, T *src) {
    *dst = *src;
}

template void copy(PublicBase *dst, PublicBase *src);
template void copy(PrivateBase *dst, PrivateBase *src);
Run Code Online (Sandbox Code Playgroud)
void copy<PublicBase>(PublicBase*, PublicBase*):
        mov     rax, QWORD PTR [rsi]
        mov     QWORD PTR [rdi], rax
        ret
void copy<PrivateBase>(PrivateBase*, PrivateBase*):
        mov     eax, DWORD PTR [rsi]
        mov     DWORD PTR [rdi], eax
        movzx   eax, BYTE PTR [rsi+4]
        mov     BYTE PTR [rdi+4], al
        ret
Run Code Online (Sandbox Code Playgroud)

问题是,C++11 中的默认复制赋值运算符何时使用按位复制而不是按成员复制?似乎既没有is_trivially_copyable也没有is_pod提供答案。

is_trivially_copyable

根据cppreference-is_trivially_copyable:

不存在潜在重叠子对象的普通可复制类型的对象是唯一可以使用 std::memcpy 安全复制的 C++ 对象。

和PublicBase都是PrivateBase普通可复制的,而不是子对象,但是PrivateBase是按成员复制而不是按位复制。

is_pod

PublicBase如果或存在派生类PrivateBase,则 的派生类PrivateBase将重用基类的填充,而 的派生类则PublicBase不会。

PrivateBase因此,以member-wise方式复制是合理的。PrivateDerived::c2否则,调用时可能会覆盖基类的填充copy<PrivateBase>(derived, base)。


class PublicDerived : public PublicBase {
public:
    char c2;
};

class PrivateDerived : public PrivateBase {
private:
    char c2;
};


int main() {
    std::cout << "sizeof(PublicBase)=" << sizeof(PublicBase) << std::endl;
    std::cout << "sizeof(PublicDerived)=" << sizeof(PublicDerived) << std::endl;
    std::cout << "sizeof(PrivateBase)=" << sizeof(PrivateBase) << std::endl;
    std::cout << "sizeof(PrivateDerived)=" << sizeof(PrivateDerived) << std::endl;

    return 0;
}
// Output:
// sizeof(PublicBase)=8
// sizeof(PublicDerived)=12
// sizeof(PrivateBase)=8
// sizeof(PrivateDerived)=8
Run Code Online (Sandbox Code Playgroud)

我对编译器如何决定是否重用基类的填充感到困惑。根据相关问题,POD类型不重用基类的填充。

根据cppreference-POD_class:

POD 类是这样的类

  • 直到 C++11:
    • 是一个聚合(没有私有或受保护的非静态数据成员),
    • 没有用户声明的复制赋值运算符,
    • 没有用户声明的析构函数,并且
    • 没有非 POD 类类型(或此类类型的数组)或引用的非静态数据成员。
  • 从 C++11 开始
    • 是一个平凡的类,
    • 是一个标准布局类(对所有非静态数据成员具有相同的访问控制),并且
    • 没有非 POD 类类型的非静态数据成员(或此类类型的数组)。

在 C++11 之前,PrivateBase不是 POD 类型(因为它具有私有数据成员),但从 C++11 开始,它变成了 POD 类型(因为它对所有非静态数据成员具有相同的访问控制)。


int main() {
    std::cout << "PublicBase: is_standard_layout=" << is_standard_layout<PublicBase>::value
              << ", is_trivial=" << is_trivial<PublicBase>::value
              << ", is_pod=" << is_pod<PublicBase>::value << std::endl;

    std::cout << "PrivateBase: is_standard_layout=" << is_standard_layout<PrivateBase>::value
              << ", is_trivial=" << is_trivial<PrivateBase>::value
              << ", is_pod=" << is_pod<PrivateBase>::value << std::endl;
}
// Output:
// PublicBase: is_standard_layout=1, is_trivial=1, is_pod=1
// PrivateBase: is_standard_layout=1, is_trivial=1, is_pod=1
Run Code Online (Sandbox Code Playgroud)

use*_*522 5

问题是,C++11 中的默认复制赋值运算符何时使用按位复制而不是按成员复制?看来 is_trivially_copyable 和 is_pod 都没有提供答案。

对术语的第一次小修正:您可能指的是隐式定义的复制赋值运算符。这与隐式声明的复制赋值运算符和默认或显式默认的复制赋值运算符不同。

隐式定义的复制赋值运算符始终使用成员方式复制,但联合除外,联合体则复制对象表示形式(即按字节方式,就像 by 一样memcpy)。

然而,填充的值是未指定的,因此如果编译器知道它确实只是填充,即不被派生类成员重用,则不需要关心覆盖它。

然后,如果编译器知道赋值运算符相当于直接复制成员的对象表示,例如,如果复制赋值运算符是微不足道的,那么它可以用整个对象的对象表示的副本来替换成员方式的副本。这不会影响任何可观察到的行为,因为唯一的区别,即生成的填充值,无论如何都未指定。即使复制分配不是微不足道的,编译器也可能在内联后发现可观察的行为不会受到此优化的影响。只要可观察的行为不会改变为抽象机上不允许的行为(“假设”规则),任何事情都是允许的。

我对编译器如何决定是否重用基类的填充感到困惑。根据相关问题,POD类型不重用基类的填充。

标准中没有规定这一点。由编译器决定在什么情况下重用填充,并且不需要与 POD 属性一致。事实上,POD 概念已被弃用,并且除了已弃用的is_pod类型特征之外,当前标准版本不再使用 POD 概念。

更重要的是,该标准规定每个基类子对象都可能重叠。此属性用于定义是否允许复制普通可复制对象memcpy,并且由于每个基类子对象都可能重叠,因此该标准理论上允许重用任何类的尾部填充。显然,这会扰乱同样是有效 C 类类型的 C 兼容性struct,因此编译器不会那么激进。

由于填充的重用会影响翻译单元之间的 ABI 兼容性,因此编译器将遵循一个一般规则来维护翻译单元之间的二进制兼容性。通常,编译器/平台组合有一个 ABI 规范。

GCC 和 Clang 遵循 Itanium C++ ABI,它指定了用于布局目的的 POD概念,明确基于 C++03 标准中的 POD 定义,排除了一些特殊情况并进行了一些说明。这个概念,而不是 C++ 标准的“POD”概念,用于决定是否在 Itanium C++ ABI 中重用尾部填充。

在C++03中PublicBase是POD,但PrivateBase不是,所以前者是出于布局目的的POD,而后者不是。因此,GCC 和 Clang 仅对后者重用尾部填充。

当尾部填充可能被重用时,编译器无法复制隐式复制赋值运算符的整个对象表示形式,因为这可能会修改派生类成员的字节,正如您已经注意到的那样,这可能会影响可观察的行为,因此不会被涵盖在“好像”之下。