了解模板的重复评估 types/values

Understanding repeated evaluation of template types/values

我有以下代码,但我不明白为什么最后一个 !has_size<Bar>::value 只有在我没有在定义 [=14= 之前注释掉完全相同的 static_assert( !has_size<Bar>::value, ...) 时才计算为真]

#include <type_traits>

template <class, class = void> struct has_size : std::false_type {};
template <class T> struct has_size<
    T, typename std::enable_if<(sizeof(T) > 0)>::type> 
    : std::true_type
{};

// expected success
struct Foo {};
static_assert( has_size<Foo>::value, "Foo doesn't have size");

struct Bar; // declare bar

// if we comment out this line, the static_assert below struct Bar{} fails
static_assert( !has_size<Bar>::value, "Bar has a size");    

struct Bar {};  // define bar

// why is this true now, but false if I comment out the previous assertion?
static_assert( !has_size<Bar>::value, "Bar has a size");

我想稍后根据 has_size<Bar> 的值做出一些模板决策。该行为在 msvc、gcc 和 clang 中是相同的。我试图弄清楚这是否是有意为之且有据可查的行为,或者我是否依靠这种行为徘徊在 UB 土地或其他灾难中。想法?

C++ 中的每个完整类型 T 都有 sizeof(T)>0 或更简单地说 sizeof(T) 是一个有效的表达式,has_size 正在使用它来检测某个类型是否完整不是,并且正在通过 SFINAE.

做到这一点

第一个static_assert

struct Bar;
static_assert( !has_size<Bar>::value, "Bar has a size"); // (1)

导致 has_size<Bar> 的实例化 Bar 不完整,这导致 has_size 的第二个特化中的测试 sizeof(T) > 0失败,此失败诉诸于使用满足 has_size<Bar>::value == false.

的主模板 has_size : std::false_type 的定义

当第二个static_assert

struct Bar {};
static_assert( !has_size<Bar>::value, "Bar has a size"); // (2)

被评估,再次请求特化 has_size<Bar>,但是这次 Bar 已经完成并且已经有 has_size<Bar> 的实例化(继承自 std::false_type), 那个专门化被使用而不是实例化一个新的,因此仍然说 has_type<Bar>::value == false.

当您评论第一个 static_assert (1) 时,在计算 (2) 的那一刻,Bar 已经定义,现在 sizeof(T) > 0 有效且真实,select has_size<Bar> : std::true_type 的专业化,现在它满足 has_type<Bar>::value == true.

不涉及UB。


为了拥有反映类型完整性变化的特征 T,您可以同意:

template <class T>
constexpr auto has_size(int) -> decltype((void)sizeof(T), bool{})
{ return true; }

template <class T>
constexpr auto has_size(...) -> bool
{ return false; }

struct Bar;
static_assert( !has_size<Bar>(0), "Bar has a size");        
struct Bar {};  // define bar    
static_assert( !has_size<Bar>(0), "Bar has a size"); // fail

您可以将 class 模板实例化视为 "cached" 或 "memoized." 更正式地说,class 模板每个翻译单元有 a single point of instantiation

所以当你写的时候:

struct Bar;
static_assert( !has_size<Bar>::value, "Bar has a size");   // #1
struct Bar {};
static_assert( !has_size<Bar>::value, "Bar has a size");   // #2

has_size<Bar>#1 实例化。那是它的 only 实例化点。在 #2,我们没有 "redo" 那个计算 - 所以它 仍然 是错误的。如果我们从不同的翻译单元再次这样做,以给出不同答案的方式,那将是格式错误的(不需要诊断),但在这种情况下 - 这是一个格式正确的程序。

当您注释掉 #1 时,现在 has_size<Bar> 的实例化点变为 #2。在程序中的 那个 点,Bar 完成,所以 has_size<Bar> 现在是 true_type... 所以静态断言触发。