constexpr和CRTP:编译器意见不一致

时间:2015-01-13 01:23:36

标签: c++ templates language-lawyer c++14 constexpr

当使用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, "");
}

3 个答案:

答案 0 :(得分:9)

operator() base static_cast执行有效thisDerived指向的派生程度最高的对象必须属于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是类类型,可以强制转换为   输入“ cv2 D的引用”,其中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_am_b的类型为base<derived>,而不是derived。换句话说,将dm_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>