Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Odd behavior passing static constexpr members without definitions by value

I was surprised to find that GCC and Clang disagree on whether to give me a linker error when passing a static constexpr member by value when there is no out-of-class definition:

#include <iostream>
#include <type_traits>
#include <typeinfo>

template <typename X>
void show(X)
{
    std::cout << typeid(X).name() << std::endl;
}

template <typename T>
struct Foo
{
    //static constexpr struct E {} nested {}; // works in gcc and clang
    //static constexpr struct E {T x;} nested {}; // doesn't work in gcc
    //static constexpr enum E {} nested {}; // works in gcc and clang
    //static constexpr enum E { FOO } nested {}; // works in gcc and clang
    //static constexpr struct E { constexpr E() {} constexpr E(const E&) {} T x=T();} nested {}; // works in gcc and clang
    static constexpr struct E { constexpr E() {} constexpr E(const E&) = default; T x=T(); } nested {}; // doesn't work in gcc
};

int main()
{
    Foo<int> x;
    show(x.nested);
}

Snippet can be played with here.

I would like to use the first line's syntax with no out-of-class definition:

static constexpr struct E {} nested {}; // works in gcc and clang

When there are no members in E, Clang and GCC only seem to care that I have no out-of-class definition of nested if I trip ODR (e.g. by taking the address). Is this standard mandated or luck?

When there are members, GCC (5.2) appears to additionally want me to have manually defined a constexpr copy constructor. Is this a bug?

From googling and SO I have found several different answers but it's hard to tease apart which are up to date with C++14. In C++98/03 I believe only integer types could be initialized inside the class. I think that C++14 expanded this to 'literal' types which includes constexpr constructable things. I don't know if this is the same as saying I am allowed to get away with not having an out-of-class definition though.

like image 949
Joseph Garvin Avatar asked Aug 05 '15 21:08

Joseph Garvin


1 Answers

So in the cases where E is a class they all looks like odr violations, if we look at cppreferences page on odr-use it says:

Informally, an object is odr-used if its address is taken, or a reference is bound to it, and a function is odr-used if a function call to it is made or its address is taken. If an object or a function is odr-used, its definition must exist somewhere in the program; a violation of that is a link-time error.

and in this case we take a reference when we call the copy constructor on this line:

show(x.nested);

It is also worth it to note that odr-violations do not require a diagnostic.

It looks what you are seeing in some cases in the effects of constructor elision with gcc if we use -fno-elide-constructors we get an error for all the cases where E is a class. In the enum cases the lvalue-to-rvalue conversion is applied and therefore there is no odr-use.

Update

dyp pointed me to defect report 1741 which questions whether binding to the reference parameter of a copy ctor is an odr-use or not:

Does this odr-use T::s, requiring it to have a definition, because of binding it to the reference parameter of S's copy constructor?

and the result was the following change to [basic.def.odr] paragraph 3:

A variable x whose name appears as a potentially-evaluated expression ex is odr-used unless x satisfies the requirements for appearing in a constant expression (5.20 [expr.const]) applying the lvalue-to-rvalue conversion (4.1 [conv.lval]) to x yields a constant expression (5.20 [expr.const]) that does not invoke any non-trivial functions and, if x is an object, ex is an element of the set of potential results of an expression e, where either the lvalue-to-rvalue conversion (4.1 [conv.lval]) is applied to e, or e is a discarded-value expression (Clause 5 [expr]). this is odr-used...

So then the question becomes which cases are covered by this change. It would seem these examples are ok:

//static constexpr struct E {} nested {}; // works in gcc and clang
//static constexpr struct E {T x;} nested {}; // doesn't work in gcc
static constexpr struct E { constexpr E() {} constexpr E(const E&) = default; T x=T(); } nested {}; // doesn't work in gcc

Since the copy ctor is trivial while this one is not trivial:

//static constexpr struct E { constexpr E() {} constexpr E(const E&) {} T x=T();} nested {}; // works in gcc and clang

We can confirm this using std::is_trivially_copyable, see it live.

The enum cases are still ok for the same reasons I stated originally.

The defect also report variance in implementation.

like image 190
Shafik Yaghmour Avatar answered Nov 04 '22 06:11

Shafik Yaghmour