在x86-64 GCC 13.1和Clang 16.0.0中,copy<PrivateBase>
函数使用按成员复制,而copy<PublicBase>
函数使用按位复制。您可以参考compiler explorer上的详细源代码和汇编代码,或者查看下面提供的代码片段:
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);
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
问题是,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
以成员方式复制是合理的。否则,基类的填充可能会在调用copy<PrivateBase>(derived, base)
时覆盖PrivateDerived::c2
。
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
我对编译器如何决定是否重用基类的填充感到困惑。根据related question,POD类型不重用基类的填充。
根据cppreference-POD_class:
POD类是一个
- 直到C++11:
- 是一个聚合(没有私有或受保护的非静态数据成员),
- 没有用户声明的复制赋值运算符,
- 没有用户声明的析构函数,并且
- 没有非POD类(或此类类型的数组)或引用类型的非静态数据成员。
- 自C++11
- 是一个平凡的类
- 是一个标准布局类(对所有非静态数据成员具有相同的访问控制),并且
- 没有非POD类类型的非静态数据成员(或此类类型的数组)。
在C11之前,PrivateBase
不是POD类型(因为它有私有数据成员),但从C11开始,它变成了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
1条答案
按热度按时间jchrr9hc1#
问题是,C11的默认复制赋值操作符何时使用按位复制而不是按成员复制?似乎is_trivially_copyable和is_pod都没有提供答案。
对术语的第一个小更正:你可能指的是 * 隐式定义的 * 复制赋值运算符。这不同于 * 隐式声明的 * 复制赋值运算符和 * 默认的 * 或 * 显式默认的 * 复制赋值运算符。
隐式定义的复制赋值运算符 always 使用成员式复制,除了联合,其对象表示被复制(即字节方式,如同
memcpy
)。然而,padding的值是未指定的,因此编译器不需要关心覆盖它,如果它知道它确实只是填充,即。不对派生类成员重用。
然后,如果编译器知道赋值运算符等价于直接复制成员的对象表示,例如如果复制赋值运算符是平凡的,则它可以用整个对象的对象表示的副本来替换成员式副本。这不会影响任何可观察到的行为,因为唯一的差异,结果填充值,无论如何都是未指定的。即使复制赋值不是平凡的,编译器也可能看到例如。在内联可观察行为不会受到该优化的影响之后。只要可观察到的行为不改变为抽象机器上不允许的行为(“as-if”规则),任何行为都是允许的。
我对编译器如何决定是否重用基类的填充感到困惑。根据相关问题,POD类型不重用基类的填充。
标准中未对此进行规定。由编译器决定在何种情况下重用填充,并且不需要与POD属性一致。事实上,POD概念已经被弃用,除了被弃用的
is_pod
类型trait之外,当前的标准版本不再使用POD概念。更重要的是,标准说 * 每个 * 基类子对象都是 * 潜在重叠的 *。这个属性用于定义是否允许
memcpy
复制一个可复制的对象,因为 * 每个 * 基类子对象都是 * 潜在重叠的 *,理论上,该标准允许 * 任何 * 类的尾部填充被重用。很明显,这会破坏C对类类型的兼容性,这些类类型也是有效的Cstruct
s,所以编译器不会那么激进。因为填充的重用影响转换单元之间的ABI兼容性,所以将存在编译器将遵循以维持转换单元之间的二进制兼容性的一般规则。通常有一个编译器/平台组合的ABI规范。
GCC和Clang遵循Itanium C ABI,它指定了 *POD的概念,用于布局 *,这明确地基于C03标准的POD定义,排除了一些特殊情况并进行了一些澄清。这个概念,而不是C标准的“POD”概念,用于决定是否在Itanium C++ ABI中重用尾填充。
在C++03中,
PublicBase
是POD,但PrivateBase
不是,因此前者是 * 用于布局的POD *,而后者不是。因此,GCC和Clang只对后者重用尾部填充。当尾部填充可能被重用时,编译器不能为隐式复制赋值操作符复制整个对象表示,因为这可能会修改派生类成员的一个字节,正如你已经注意到的那样,这可能会影响可观察的行为,因此不会在“as-if”下覆盖。