17

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.

Joseph Garvin
  • 20,727
  • 18
  • 94
  • 165
  • 1
    Interesting. Your final test case works with gcc -O2 and clang -O3, but not clang -O2 or gcc -O1. Your 2nd and 5th test case also don't work at -O0 on either compiler. Fun times...(also, the part of [basic.def.odr] on what counts and doesn't count as an odr-use makes my head hurt). – T.C. Aug 05 '15 at 22:19
  • Yeah I wouldn't consider pass by value to be an odr-use, since it shouldn't rely on the object's identity... but I don't have sufficient language-lawyer foo to discern. – Joseph Garvin Aug 05 '15 at 22:29
  • 1
    Note that odr [violations do not require a diagnostic](http://stackoverflow.com/a/28446388/1708801) so both can be correct. The history of how we got the [current odr rules is kind of twisted too](http://stackoverflow.com/q/31565836/1708801). This is a curious case. – Shafik Yaghmour Aug 10 '15 at 03:26
  • 1
    I think I may know what's going on: when you pass by value, you invoke the copy constructor, and copy constructors are defined as taking a reference, so passing an object by value *implicitly also causes you to pass by reference to the copy constructor*. It just so happens that specifically the default generated copy constructors for empty classes in Clang/GCC don't trip the usual pass-by-reference-violates-odr-check. Unless the standard makes an explicit exception for this, which would be nice, since the object identity really isn't needed for defaulted POD copy constructors at all. – Joseph Garvin Aug 10 '15 at 14:13

1 Answers1

7

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.

Community
  • 1
  • 1
Shafik Yaghmour
  • 154,301
  • 39
  • 440
  • 740
  • I'm not entirely sure if copy ctors always lead to odr-usage, there's a weird rule I haven't fully understood yet. David Krauss has mentioned it [in a discussion about ODR](https://groups.google.com/a/isocpp.org/d/msg/std-discussion/rmJL-TzjlVY/7wGk34XaMHoJ), and later referred to http://wg21.cmeerw.net/cwg/issue1741 – dyp Aug 14 '15 at 16:15
  • @dyp let me look at those then – Shafik Yaghmour Aug 14 '15 at 17:17
  • @dyp interesting, so the proposed langauage appears in C++14 draft but the DR was moved to open, so not sure what to make of that. – Shafik Yaghmour Aug 14 '15 at 17:23
  • Yes, and the example is a bit tricky since [expr.call]p7 says that binding to `...` causes l-t-r; but there's no equivalent requirement for passing to a by-value parameter. – dyp Aug 14 '15 at 17:30
  • Yeah I'd love to know if this language made it into the final draft because then we'd know whether GCC/clang need a bug report. – Joseph Garvin Aug 17 '15 at 14:12
  • @JosephGarvin apologizes for the late response, I have been away. This language was part of the C++14 draft and is present in the latest draft as well. So that would seem to indicate gcc is incorrect in this case but the defect report has been moved to open which perhaps means there will be more changes. Let me think about it some more but a gcc bug report probably makes sense. – Shafik Yaghmour Aug 31 '15 at 12:04