PAYATHON 2026

为什么 GCC、Clang 与 MSVC 对 trivially copyable 的判断不同?

数据小许

结论:按标准定义,本例应为 false

ILT_NotTCopy 不满足 trivially copyable 的要求,std::is_trivially_copyable_v<ILT_NotTCopy> 应当为 false。题目中 MSVC 的结果符合这一规则,GCC 和 Clang 返回 true 则与标准定义不一致。

题目没有给出编译器版本、目标平台和编译选项,因此不能把这组观察推广到所有版本,也不能据此判断具体版本是否已经修复。

为什么删除全部复制和移动操作会使条件不成立?

标准草案的 class.prop 要求 trivially copyable class 同时满足:

  1. 至少有一个 eligible 的复制构造函数、移动构造函数、复制赋值运算符或移动赋值运算符。
  2. 上述所有 eligible 操作都是 trivial。
  3. 析构函数是 trivial 且没有被删除。

special 还要求 eligible special member function 不能是 deleted;涉及约束时,还需要满足相应的约束条件。

原代码中的四个操作全部声明为 = delete,所以没有 eligible 的复制或移动操作,第一项条件不成立。即使“所有 eligible 操作都是 trivial”在集合为空时成立,也不能因此认定整个类型满足定义。

隐式声明的析构函数虽然是 trivial 且未被删除,也无法补足缺少的复制或移动操作。默认构造函数不在第一项列出的四种操作之中,因此 ILT_NotTCopy() = default; 也不能满足这一条件。

GCC 和 Clang 为什么仍可能返回 true?

这类差异与 trivially copyable 定义的历史修订及实现支持有关。CWG 1734 的修订明确加入了“至少有一个未删除的复制或移动构造函数或赋值运算符”的要求。

Clang 的官方问题记录 LLVM #38398 包含同类情况:所有复制和移动操作均被删除,但类型特征仍返回 true。维护者将该问题关联到 DR1734,后续讨论还涉及修改判断可能带来的 ABI 兼容性影响。

成员只有一个 int,或者对象看起来可以按字节复制,都不足以绕过标准对特殊成员函数的要求。对于题目给出的类型,缺少 eligible 复制或移动操作已经足以得到 false。

为什么空的复制构造函数体会改变结果?

将原来的删除声明替换为:

ILT_NotTCopy(ILT_NotTCopy const&) {}

会产生两个变化:

  • 复制构造函数不再是 deleted,在本例中成为 eligible。
  • 它是 user-provided 的复制构造函数,因此不是 trivial。

修改前,类型违反“至少有一个 eligible 操作”的要求;修改后,类型违反“所有 eligible 操作都是 trivial”的要求。两种情况下,标准要求的结果都是 false,只是原因不同。

空函数体也不是默认复制行为。例如:

ILT_NotTCopy source{};
source.val = 42;
ILT_NotTCopy target(source);

使用上述空复制构造函数时,source.val 不会被复制给 target.val,后者没有被初始化。因此,不应为了让类型特征返回 false 就加入这个函数体:它会允许原本禁止的复制操作,并改变对象的初始化行为。

implicit-lifetime 与这个结论是否矛盾?

两者不矛盾,检查的条件不同。

class.prop 对 implicit-lifetime class 的一条判定路径是:具有至少一个 trivial 且 eligible 的构造函数,并具有 trivial、未删除的析构函数。这里可以使用默认构造函数。

本例的默认构造函数满足这条路径,所以该类型可以是 implicit-lifetime class,同时不属于 trivially copyable class。

还需要区分声明和定义:

ILT_NotTCopy() = default;

这是用户显式声明、在首次声明时显式 defaulted 的默认构造函数。它不是 implicitly-declared,也不是 user-provided。编译器会在需要时提供隐式定义。相关区别见 dcl.fct.def.default,默认构造函数的 trivial 条件见 class.default.ctor。

实际代码中怎样排查和处理?

保留原类型定义,记录编译器版本、标准模式、目标平台和标准库,再用以下断言区分“能否执行复制或移动”与“是否 trivially copyable”:

using T = ILT_NotTCopy;

static_assert(!std::is_copy_constructible_v<T>);
static_assert(!std::is_move_constructible_v<T>);
static_assert(!std::is_copy_assignable_v<T>);
static_assert(!std::is_move_assignable_v<T>);

// 按上述标准规则应当通过;可用于检测所用工具链的差异。
static_assert(!std::is_trivially_copyable_v<T>);

这些检查用于复现和验证,不代表已经在某组编译器版本上实际运行过。代码使用 _v 形式,需要 C++17 或更新的语言模式。

如果业务需要判断某个复制或移动表达式是否合法,应使用对应的 is_copy_constructible、is_move_constructible 或赋值特征。它们与 is_trivially_copyable 的含义不同,不能作为后者的通用替代。

对于原本就要求禁止复制和移动的类型,应继续保留 = delete,并在依赖该类型特征的代码路径中处理工具链差异。不要通过空复制构造函数来改变查询结果,也不要为标准类型特征添加自定义特化。

备注:内容仅供参考。