Clang and GCC vs MSVC and ICC: Is a static_assert in the copy/move constructor required to work, if copy/move elision could apply too?
Asked Answered
K

4

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?

Katharinekatharsis answered 2/4, 2016 at 9:35 Comment(9)
good question. I can't find the answer in the standard.Smackdab
I'd argue that this could be considered a defect in the standard. What do different optimisation levels lead to?Longevity
@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.Katharinekatharsis
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?Sauncho
@Sauncho 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.Katharinekatharsis
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.Carnahan
@Carnahan Yeah that's true, but what if it was 2 instead of 1?Razzledazzle
@Carnahan The behavior of the compilers does not change if I change the condition to something that can be fulfilled, e.g. std::is_same<void, T>::value. I changed the text accordingly. Also, X<int> is a valid type. Just the move constructor is armed with the static_assert.Katharinekatharsis
I would recommend to add the 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.Celebes
P
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.

Proctology answered 11/4, 2016 at 12:43 Comment(12)
If the definition does not exist, then there is not "only one definition". There are "zero definitions".Polyphagia
§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."Polyphagia
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 :-(.Proctology
You have to read the whole definition, not just part of it :PPolyphagia
However, I'm not convinced that static_assertion 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.Polyphagia
Okay, that's the case in the question. :)Polyphagia
@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.Razzledazzle
@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).Katharinekatharsis
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.Polyphagia
@LightnessRacesinOrbit: I am pretty sure that this has nothing to do with SFINAE. Member functions of templates only get instantiated when actually used.Katharinekatharsis
@Rumburak: Ah right yes good point. Although.. beware MSVS doesn't have two-phase lookup. Would that break this?Polyphagia
@LightnessRacesinOrbit Well, MSVC behaves differently from clang and gcc. I'll submit a bug report and see what they have to say :-)Katharinekatharsis
S
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…

Suksukarno answered 5/4, 2016 at 11:48 Comment(3)
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.Katharinekatharsis
Move elision or not, the return plainly odr-uses the move constructor, so the ODR requires a definition to exist.Carnahan
@Carnahan Thanks, this comment led me to the right section in the standard, I think. Care to add an answer?Katharinekatharsis
R
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>).

Ruppert answered 5/4, 2016 at 7:5 Comment(3)
While most of that is true (no compiler evaluates the method when X<int> 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.Katharinekatharsis
@Rumburak, I hope this is official enough and it states copy elision to be optionalRuppert
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.Katharinekatharsis
E
-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

Emmett answered 2/4, 2016 at 15:2 Comment(3)
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.Katharinekatharsis
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.Emmett
See here for instance: en.cppreference.com/w/cpp/language/… 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.Katharinekatharsis

© 2022 - 2024 — McMap. All rights reserved.