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_am_b 的类型是 base<derived>,而不是 derived。换句话说,将其复制到 m_am_b.

时,您有 sliced d

对于base中的operator()做一个有效的static_castthis指向的最派生对象必须是Derived类型(或其子类)。但是,e 的成员属于 base<derived> 类型,而不是 derived 本身。在行

const noexcept { return m_a() + m_b(); }

m_abase<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_casts,以及有 foo return 一个 expr<D1, D2> 而不是 expr<base<D1>, base<D2>>.