Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

constexpr and CRTP: compiler disagreement

When expression templates are implemented using CRTP, the class at the top of the expression hierarchy uses base-to-derived downcasting in order to implement some of its operations. According to clang-3.5 (-std=c++1y), this downcast should be illegal in constexpr functions:

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 happily compiles the code. So who is right? If Clang is right, which C++14 restriction on constexpr functions makes this downcasting illegal?

Here's the 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, "");
}
like image 807
void-pointer Avatar asked Jan 13 '15 01:01

void-pointer


2 Answers

For the operator() in base to do a valid static_cast, the most-derived object that this points to must be of type Derived (or a subclass thereof). However, the members of e are of type base<derived>, not derived itself. In the line

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

m_a is of type base<derived>, and base<derived>::operator() is called - with a most-derived object of type base<derived>.
Thus the cast tries to cast *this to a reference to object type that it doesn't actually refer to; That operation would have undefined behavior as [expr.static.cast]/2 describes:

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.

And subsequently, [expr.const]/2 applies:

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

Instead, rewrite foo as follows:

template <class D1, class D2>
constexpr auto foo(const D1& d1, const D2& d2)
noexcept { return expr<D1, D2>{d1, d2}; }

And the code works fine.

like image 136
Columbo Avatar answered Nov 10 '22 00:11

Columbo


It seems to me that Clang is right in this case. The type of e is const expr<base<derived>, base<derived>>, so m_a and m_b have type base<derived>, rather than derived. In other words, you have sliced d when copying it into m_a and m_b.

like image 40
Tavian Barnes Avatar answered Nov 10 '22 00:11

Tavian Barnes