25

I have a static_assert in a move constructor of a template struct of mine. Is this static_assert required to be considered by the compiler, even if copy elision is possible?

This is the stripped-down scenario:

#include <type_traits>

template<typename T>
struct X
{
  X(X&&) { static_assert(std::is_same<void, T>::value, "Intentional Failure"); }
};

auto impl() -> X<int>;    
auto test() -> decltype(impl())
{
  return impl();
}

int main()
{
  test();
}

GCC and Clang agree to evaluate the static_assert and fail to compile.
MSCV and ICC on the other hand compile the code just fine.

Interestingly, when I remove the definition of the move constructor and just declare it like this:

template<typename T>
struct X
{
  X(X&&);
};

GCC and Clang also compile the code now. Thus, all compilers seem to agree that the definition of the move constructor is irrelevant for copy elision.

Question:
If there is a static_assert in the copy/move constructor, does the standard require it to be evaluated even if copy/move elision is possible?

Rumburak
  • 3,416
  • 16
  • 27
  • 1
    good question. I can't find the answer in the standard. – Richard Hodges Apr 02 '16 at 12:32
  • I'd argue that this could be considered a defect in the standard. What do different optimisation levels lead to? – Daniel Jour Apr 02 '16 at 18:41
  • @DanielJour I tested gcc-5.2, clang-3.4, clang-3.7 and icc-13.0.1 (at gcc.godbolt.org) with -O0 and -O3, msvc (at webcompiler.cloudapp.net) with /Ot and /Ox. They keep their respective behavior regardless of those optimization flags. – Rumburak Apr 02 '16 at 19:59
  • 1
    At first, I couldn't get what your Q was. Hence made some changes to make it understandable. Please review, if the meaning of your Q is not changed anyhow. BTW, the compiler should always check the definition if they exist, irrespective of copy/move elision, right? So why do you feel that it can be skipped, even for templates? – iammilind Apr 05 '16 at 11:41
  • @iammilind Thanks! I made a minor correction. As far as I understand, the respective constructor must be declared and accessible for copy/move elision to apply. That's why all compilers are gladly compiling the code, once you remove the definition. – Rumburak Apr 05 '16 at 11:52
  • The test case is ill-formed NDR as there's no value of `T` that would not trigger the `static_assert`, so no valid specialization can be generated for `X`. – T.C. Apr 09 '16 at 16:10
  • @T.C. Yeah that's true, but what if it was `2` instead of `1`? – Barry Apr 09 '16 at 18:50
  • @T.C. The behavior of the compilers does not change if I change the condition to something that can be fulfilled, e.g. `std::is_same::value`. I changed the text accordingly. Also, `X` is a valid type. Just the move constructor is armed with the `static_assert`. – Rumburak Apr 09 '16 at 19:21
  • I would recommend to add the tag [tag:language-lawyer] to attract more specialists. Though I'm afraid it's too late for the bounty, and I wouldn't know which of the current tags you could sacrifice, as they are all relevant. – Fabio says Reinstate Monica Apr 11 '16 at 12:52

4 Answers4

4

The following should help.

You do not have to employ type deduction to illustrate the problem. Even the simpler example has the same issue:

#include <type_traits>

template <typename T>
struct X
{
  X() {}
  X(X&&) { static_assert(std::is_same<void, T>::value, "failed"); }
};

int main()
{
  X<int> x = X<int>();
}

Clang and GCC will not compile it. MSVC compiles and executes fine.

This shows that the problem is related to odr-use and when definitions of member functions are instantiated.

14.7.1 [temp.inst] paragraph 2 says "[...] the specialization of the member is implicitly instantiated when the specialization is referenced in a context that requires the member definition to exist"

3.2 [basic.def.odr] paragraph 3 says (in a note) "[...] A constructor selected to copy or move an object of class type is odr-used even if the call is actually elided by the implementation"

3.2 [basic.def.odr] paragraph 4 says "Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program; no diagnostic required."

Ergo: the specialization should be instantiated, and the assertion have fired.

Andrzej
  • 5,027
  • 27
  • 36
  • If the definition does not exist, then there is not "only one definition". There are "zero definitions". – Lightness Races in Orbit Apr 11 '16 at 12:51
  • §3.2/4: "Every program shall contain exactly one definition of every non-inline function or variable that is odr-used in that program; no diagnostic required." – Lightness Races in Orbit Apr 11 '16 at 12:53
  • **3.2 paragraph 1**: "No translation unit shall contain more than one definition of [...]" -- it seams that 0 definitions is fine with one-definition rule :-(. – Andrzej Apr 11 '16 at 12:53
  • You have to read the whole definition, not just part of it :P – Lightness Races in Orbit Apr 11 '16 at 12:54
  • _However_, I'm not convinced that `static_assert`ion failure prevents the surrounding definition from actually existing. Clause 7 teaches us that evaluating such a declaration causes the program to be ill-formed if it evaluates to `false`, but not that it causes the program to obliterate all evidence to its ever having existed in the first place. I imagine SFINAE takes care of this for templates, though, as long as the assertion is written in terms of a template argument. – Lightness Races in Orbit Apr 11 '16 at 12:55
  • Okay, that's the case in the question. :) – Lightness Races in Orbit Apr 11 '16 at 12:57
  • @LightnessRacesinOrbit Is it not sufficient that (1) the move ctor must be instantiated and (2) instantiating the move ctor must fire the `static_assert`? Not sure what else would be missing. – Barry Apr 11 '16 at 14:23
  • @LightnessRacesinOrbit I am confused by the comments. Do you think the paragraphs listed by Andrzej are not saying that the `static_assert` should fire? The question what should happen in case of a missing definition is probably a different one (according to the response I received for the clang bug report, the definition is required by the standard but not by any known compiler). – Rumburak Apr 11 '16 at 14:26
  • Hmm nah you guys are probably right. Though I stand by the notion that only SFINAE here saves you from the entire program becoming ill-formed anyway. – Lightness Races in Orbit Apr 11 '16 at 14:31
  • @LightnessRacesinOrbit: I am pretty sure that this has nothing to do with SFINAE. Member functions of templates only get instantiated when actually used. – Rumburak Apr 11 '16 at 14:45
  • @Rumburak: Ah right yes good point. Although.. beware MSVS doesn't have two-phase lookup. Would that break this? – Lightness Races in Orbit Apr 11 '16 at 14:49
  • @LightnessRacesinOrbit Well, MSVC behaves differently from clang and gcc. I'll submit a bug report and see what they have to say :-) – Rumburak Apr 11 '16 at 15:03
2

I believe the answer is no. My logic goes like this:

  1. Copy elision requires declaration of copy/move constructors but doesn't require definition.
  2. Member function definitions of templates are not instantiated unless their definitions are required.
  3. If a definition is not instantiated it cannot be tested for being ill-formed.

References:

14.7.1.1 …The implicit instantiation of a class template specialization causes the implicit instantiation of the declarations, but not of the definitions, default arguments, or exception-specifications of the class member functions…

14.7.1.2 Unless a member of a class template… has been explicitly instantiated or explicitly specialized, the specialization of the member is implicitly instantiated when the specialization is referenced in a context that requires the member definition to exist…

grisumbras
  • 850
  • 2
  • 6
  • 11
  • Following this answer, I reported bugs for gcc and clang. According to clang developers, the definition is actually required for copy elision. This would switch your answer to "yes". I haven't found the part in the standard that proves/disproves their point. gcc developers so far are less adamant. – Rumburak Apr 07 '16 at 13:12
  • Move elision or not, the return plainly odr-uses the move constructor, so the ODR requires a definition to exist. – T.C. Apr 09 '16 at 16:14
  • @T.C. Thanks, this comment led me to the right section in the standard, I think. Care to add an answer? – Rumburak Apr 09 '16 at 19:31
1

move constructors are not called. static_assert is evaluated upon instantiation of X<int>::X(X&&). Most probably, some compilers evaluate template methods upon use (when you use move constructor, and you don't use it), and others - upon instantiation of class template (when you first use X<int>).

Andrei R.
  • 2,374
  • 1
  • 13
  • 27
  • 1
    While most of that is true (no compiler evaluates the method when `X` is used first), it does not answer the question. The question is: Does the standard require one behavior or the other? Or does the standard leave this to the implementation? Or maybe is this a known defect in the standard? Any kind of "official" reference would be appreciated. – Rumburak Apr 05 '16 at 08:26
  • @Rumburak, I hope [this](http://en.cppreference.com/w/cpp/language/copy_elision) is official enough and it states copy elision to be optional – Andrei R. Apr 05 '16 at 09:24
  • The question is not about whether or not the copy/move can be elided. It is about whether or not the compiler is required to look into the definition and see the static_assert. All compilers elide the move, if the definition is missing. But only two out of four compilers look into the definition if it is present. – Rumburak Apr 05 '16 at 09:36
-1

I think the answer is : yes.

first the static_assert forces the constructor from "definition" to a declaration.

I am not sure exactly about the template nature of the static_assert with regard to the the 12.8 section below either...

(I apologize for the formatting...)

c © ISO/IEC N3242=11-0012 7 Declarations [dcl.dcl]

2. A declaration is a definition unless it declares a function without specifying the function’s body (8.4), it contains the extern specifier (7.1.1) or a linkage-specification 25(7.5) and neither an initializer nor a function-body, it declares a static data member in a class definition (9.4), it is a class name declaration (9.1), it is an opaque-enum-declaration(7.2), or it is a typedef declaration (7.1.3), a using-declaration(7.3.3), a static_assert-declaration(Clause 7), an attribute-declaration (Clause 7), an empty-declaration (Clause 7), or a using-directive (7.3.4)

12.8 Copying and moving class objects [class.copy]

7 A member function template is never instantiated to perform the copy of a class object to an object of its class type. [Example:

struct S {
template<typename T> S(T);
template<typename T> S(T&&);
S();
};

S f();
const S g;

void h() {
S a( f() );// does not instantiate member template;
// uses the implicitly generated move constructor


S b(g);// does not instantiate the member template;
// uses the implicitly generated copy constructor
}

— end example ]

32 When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization. 123 - This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies): — in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

William Jones
  • 204
  • 1
  • 5
  • 1
    I don't see how (7) is relevant as the move constructor is not a member template and the compiler won't generate a copy/move constructor if a move constructor is declared. I also don't see the relevance of (32) because the copy/move constructor still need to be legal to use. For instance, no compiler will elide copy/move if the copy/move constructor is deleted. And I am unclear yet, what the impact of (2) might be? The `static_assert` is a declaration inside a definition. – Rumburak Apr 02 '16 at 20:22
  • I am confused as to how your move constructor is not a template member. I would expect that the instantiated moves would depend on the type T since the static_assert is evaluated. I believe that 7.2 implies the move constructor you created is a template by Clause 7... The fact that the move constructor is not explicitly templated does not remove the requirement to create an explicitly typed call. How would a compiler handle the explicit instantiation? Of course I could be totally wrong on this. – William Jones Apr 03 '16 at 12:22
  • See here for instance: http://en.cppreference.com/w/cpp/language/member_template#Member_function_templates It is not the standard, but close enough ;-) A template member is a member function that is a template itself. The move constructor in the question is a non-template member of a template struct. – Rumburak Apr 03 '16 at 18:55