constexpr 和 CRTP:编译器分歧
constexpr and CRTP: compiler disagreement
当使用 CRTP 实现表达式模板时,表达式层次结构顶部的 class 使用从基到派生的向下转换来实现它的一些操作。根据 clang-3.5 (-std=c++1y
),这种向下转换在 constexpr
函数中应该是非法的:
test.cpp:42:16: error: static_assert expression is not an integral constant expression
static_assert(e() == 0, "");
^~~~~~~~
test.cpp:11:26: note: cannot cast object of dynamic type 'const base<derived>' to type 'const derived'
const noexcept { return static_cast<const Derived&>(*this)(); }
GCC开心compiles the code.那么谁说的对呢?如果 Clang 是正确的,那么 constexpr
函数的哪个 C++14 限制使得这种向下转型是非法的?
这是 MWE:
template <class Derived>
class base
{
public:
constexpr auto operator()()
const noexcept { return static_cast<const Derived&>(*this)(); }
};
class derived : public base<derived>
{
public:
constexpr auto operator()()
const noexcept { return 0; }
};
template <class A, class B>
class expr : public base<expr<A, B>>
{
const A m_a;
const B m_b;
public:
constexpr explicit expr(const A a, const B b)
noexcept : m_a(a), m_b(b) {}
constexpr auto operator()()
const noexcept { return m_a() + m_b(); }
};
template <class D1, class D2>
constexpr auto foo(const base<D1>& d1, const base<D2>& d2)
noexcept { return expr<base<D1>, base<D2>>{d1, d2}; }
int main()
{
constexpr auto d = derived{};
constexpr auto e = foo(d, d);
static_assert(e() == 0, "");
}
在我看来,Clang 在这种情况下是正确的。 e
的类型是 const expr<base<derived>, base<derived>>
,因此 m_a
和 m_b
的类型是 base<derived>
,而不是 derived
。换句话说,将其复制到 m_a
和 m_b
.
时,您有 sliced d
对于base
中的operator()
做一个有效的static_cast
,this
指向的最派生对象必须是Derived
类型(或其子类)。但是,e
的成员属于 base<derived>
类型,而不是 derived
本身。在行
const noexcept { return m_a() + m_b(); }
m_a
是 base<derived>
类型,并且 base<derived>::operator()
被调用 - 具有 base<derived>
[=49= 类型的最派生对象].
因此,强制转换会尝试将 *this
强制转换为对它实际上并未引用的对象类型的引用;该操作将具有未定义的行为,如 [expr.static.cast]/2 所述:
An lvalue of type “cv1 B
,” where B is a class type, can be cast to
type “reference to cv2 D
,” where D
is a class
derived (Clause 10) from B
[..]. If the object of type “cv1 B” is actually a subobject of an object of type D
, the result refers
to the enclosing object of type D
. Otherwise, the behavior is
undefined.
随后,[expr.const]/2 适用:
A conditional-expression e
is a core constant expression unless
the evaluation of e
, following the rules of the abstract machine
(1.9), would evaluate one of the following expressions:
(2.5) — an operation that would have undefined behavior
改写 foo
如下:
template <class D1, class D2>
constexpr auto foo(const D1& d1, const D2& d2)
noexcept { return expr<D1, D2>{d1, d2}; }
和代码 works fine.
这是对您的原始代码的一个体面的修改。 UB 已删除,并且它很好地扩展:
namespace library{
template <class Derived>
class base
{
public:
constexpr Derived const& self() const noexcept { return static_cast<const Derived&>(*this); }
constexpr auto operator()()
const noexcept { return self()(); }
};
template <class A, class B>
class expr : public base<expr<A, B>>
{
const A m_a;
const B m_b;
public:
constexpr explicit expr(const A a, const B b)
noexcept : m_a(a), m_b(b) {}
constexpr auto operator()()
const noexcept { return m_a() + m_b(); }
};
template <class D1, class D2>
constexpr auto foo(const base<D1>& d1, const base<D2>& d2)
noexcept { return expr<D1, D2>{d1.self(), d2.self()}; }
}
namespace client {
class derived : public library::base<derived> {
public:
constexpr auto operator()()
const noexcept { return 0; }
};
}
int main()
{
constexpr auto d = client::derived{};
constexpr auto e = foo(d, d);
static_assert(e() == 0, "");
}
基本上每个base<X>
必须是一个X
。所以当你存储它时,你将它存储为 X
,而不是 base<X>
。我们可以通过 base<X>::self()
以 constexpr
的方式访问 X
。
这样一来,我们就可以把机器装进namespace library
了。 foo
可以通过 ADL 找到,如果您(例如)开始像代码一样向表达式模板添加运算符,您将不必手动导入它们以使 main
工作。
您的 derived
是由客户端代码为您的库创建的 class,因此进入另一个名称空间。它会根据需要覆盖 ()
,并且 "just works".
一个不那么做作的示例将 foo
替换为 operator+
,并且这种样式的优势变得显而易见。 main
变为 constexpr auto e = d+d;
而不必 using library::operator+
.
所做的更改是将 self()
方法添加到 base
以访问 Derived
,使用它删除 ()
中的 static_cast
s,以及有 foo
return 一个 expr<D1, D2>
而不是 expr<base<D1>, base<D2>>
.
当使用 CRTP 实现表达式模板时,表达式层次结构顶部的 class 使用从基到派生的向下转换来实现它的一些操作。根据 clang-3.5 (-std=c++1y
),这种向下转换在 constexpr
函数中应该是非法的:
test.cpp:42:16: error: static_assert expression is not an integral constant expression
static_assert(e() == 0, "");
^~~~~~~~
test.cpp:11:26: note: cannot cast object of dynamic type 'const base<derived>' to type 'const derived'
const noexcept { return static_cast<const Derived&>(*this)(); }
GCC开心compiles the code.那么谁说的对呢?如果 Clang 是正确的,那么 constexpr
函数的哪个 C++14 限制使得这种向下转型是非法的?
这是 MWE:
template <class Derived>
class base
{
public:
constexpr auto operator()()
const noexcept { return static_cast<const Derived&>(*this)(); }
};
class derived : public base<derived>
{
public:
constexpr auto operator()()
const noexcept { return 0; }
};
template <class A, class B>
class expr : public base<expr<A, B>>
{
const A m_a;
const B m_b;
public:
constexpr explicit expr(const A a, const B b)
noexcept : m_a(a), m_b(b) {}
constexpr auto operator()()
const noexcept { return m_a() + m_b(); }
};
template <class D1, class D2>
constexpr auto foo(const base<D1>& d1, const base<D2>& d2)
noexcept { return expr<base<D1>, base<D2>>{d1, d2}; }
int main()
{
constexpr auto d = derived{};
constexpr auto e = foo(d, d);
static_assert(e() == 0, "");
}
在我看来,Clang 在这种情况下是正确的。 e
的类型是 const expr<base<derived>, base<derived>>
,因此 m_a
和 m_b
的类型是 base<derived>
,而不是 derived
。换句话说,将其复制到 m_a
和 m_b
.
d
对于base
中的operator()
做一个有效的static_cast
,this
指向的最派生对象必须是Derived
类型(或其子类)。但是,e
的成员属于 base<derived>
类型,而不是 derived
本身。在行
const noexcept { return m_a() + m_b(); }
m_a
是 base<derived>
类型,并且 base<derived>::operator()
被调用 - 具有 base<derived>
[=49= 类型的最派生对象].
因此,强制转换会尝试将 *this
强制转换为对它实际上并未引用的对象类型的引用;该操作将具有未定义的行为,如 [expr.static.cast]/2 所述:
An lvalue of type “cv1
B
,” where B is a class type, can be cast to type “reference to cv2D
,” whereD
is a class derived (Clause 10) fromB
[..]. If the object of type “cv1 B” is actually a subobject of an object of typeD
, the result refers to the enclosing object of typeD
. Otherwise, the behavior is undefined.
随后,[expr.const]/2 适用:
A conditional-expression
e
is a core constant expression unless the evaluation ofe
, following the rules of the abstract machine (1.9), would evaluate one of the following expressions:(2.5) — an operation that would have undefined behavior
改写 foo
如下:
template <class D1, class D2>
constexpr auto foo(const D1& d1, const D2& d2)
noexcept { return expr<D1, D2>{d1, d2}; }
和代码 works fine.
这是对您的原始代码的一个体面的修改。 UB 已删除,并且它很好地扩展:
namespace library{
template <class Derived>
class base
{
public:
constexpr Derived const& self() const noexcept { return static_cast<const Derived&>(*this); }
constexpr auto operator()()
const noexcept { return self()(); }
};
template <class A, class B>
class expr : public base<expr<A, B>>
{
const A m_a;
const B m_b;
public:
constexpr explicit expr(const A a, const B b)
noexcept : m_a(a), m_b(b) {}
constexpr auto operator()()
const noexcept { return m_a() + m_b(); }
};
template <class D1, class D2>
constexpr auto foo(const base<D1>& d1, const base<D2>& d2)
noexcept { return expr<D1, D2>{d1.self(), d2.self()}; }
}
namespace client {
class derived : public library::base<derived> {
public:
constexpr auto operator()()
const noexcept { return 0; }
};
}
int main()
{
constexpr auto d = client::derived{};
constexpr auto e = foo(d, d);
static_assert(e() == 0, "");
}
基本上每个base<X>
必须是一个X
。所以当你存储它时,你将它存储为 X
,而不是 base<X>
。我们可以通过 base<X>::self()
以 constexpr
的方式访问 X
。
这样一来,我们就可以把机器装进namespace library
了。 foo
可以通过 ADL 找到,如果您(例如)开始像代码一样向表达式模板添加运算符,您将不必手动导入它们以使 main
工作。
您的 derived
是由客户端代码为您的库创建的 class,因此进入另一个名称空间。它会根据需要覆盖 ()
,并且 "just works".
一个不那么做作的示例将 foo
替换为 operator+
,并且这种样式的优势变得显而易见。 main
变为 constexpr auto e = d+d;
而不必 using library::operator+
.
所做的更改是将 self()
方法添加到 base
以访问 Derived
,使用它删除 ()
中的 static_cast
s,以及有 foo
return 一个 expr<D1, D2>
而不是 expr<base<D1>, base<D2>>
.