当使用CRTP实现表达式模板时,表达式层次结构顶部的类使用从基础到派生的向下转换来实现其某些操作。根据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, "");
}
答案 0 :(得分:9)
operator()
base
static_cast
执行有效this
,Derived
指向的派生程度最高的对象必须属于e
类型(或其子类)。但是,base<derived>
的成员属于derived
,而非const noexcept { return m_a() + m_b(); }
。在行
m_a
base<derived>
属于base<derived>::operator()
类型,而base<derived>
被称为 - ,其中派生出来的对象类型为*this
。
因此,演员试图将B
转换为对它实际上没有引用的对象类型的引用;该操作将具有未定义的行为,因为[expr.static.cast] / 2描述:
类型为“ cv1
D
的左值”,其中B是类类型,可以强制转换为 输入“ cv2D
的引用”,其中B
是一个类 来自D
[...]的派生(第10条)。 如果“ cv1 B”类型的对象实际上是D
类型对象的子对象,则结果引用 到e
类型的封闭对象。 否则,行为是 未定义。强>
随后,[expr.const] / 2适用:
条件表达式
e
是核心常量表达式,除非foo
的评估,遵循抽象机器的规则 (1.9),将评估以下表达式之一:(2.5) - 一个具有未定义行为的操作
相反,重写template <class D1, class D2>
constexpr auto foo(const D1& d1, const D2& d2)
noexcept { return expr<D1, D2>{d1, d2}; }
如下:
{{1}}
代码works fine。
答案 1 :(得分:5)
在我看来,Clang在这种情况下是正确的。 e
的类型为const expr<base<derived>, base<derived>>
,因此m_a
和m_b
的类型为base<derived>
,而不是derived
。换句话说,将d
和m_a
复制到m_b
时,您有sliced {{1}}。
答案 2 :(得分:2)
这是对原始代码的一个不错的返工。 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>
。我们可以X
方式通过base<X>::self()
访问constexpr
。
通过这种方式,我们可以将机制放入namespace library
。可以通过ADL找到foo
,如果您(例如)开始向表达式模板添加运算符(如代码),则无需手动导入它们以使main
起作用。
您的derived
是由您的库的客户端代码创建的类,因此在另一个名称空间中。它会随心所欲地覆盖()
,并且“只是有效”。
一个不太做作的例子会将foo
替换为operator+
,这种风格的优势变得明显。 main
成为constexpr auto e = d+d;
,而不必using library::operator+
。
所做的更改是向self()
添加base
方法以访问Derived
,使用该方法移除static_cast
中的()
,并{ {1}}返回foo
而不是expr<D1, D2>
。