Confusing behaviour around mandatory elision of copy/move [duplicate] - c++

The following code behaves differently with or without user-defined copy constructor under GCC 8.0.1:
#include <cassert>
struct S {
int i;
int *p;
S() : i(0), p(&i) {}
// S(const S &s) : i(s.i), p(&i) {} // #1
// S(const S &s) : i(s.i), p(s.p) {} // #2
// S(const S &s) = delete; // #3
};
S make_S() {return S{};}
int main()
{
S s = make_S();
assert(s.p == &s.i);
}
With either of the commented user-defined copy constructors (even with #2, the one performing a simple shallow copy), the assertion will not fail, which means guaranteed copy elision works as expected.
However, without any user-defined copy constructor, the assertion fails, which means the object s in main function is not default-constructed. Why does this happen? Doesn't guaranteed copy elision perform here?

Quoting from C++17 Working Draft §15.2 Temporary Objects Paragraph 3 (https://timsong-cpp.github.io/cppwp/class.temporary#3):
When an object of class type X is passed to or returned from a function, if each copy constructor, move constructor, and destructor of X is either trivial or deleted, and X has at least one non-deleted copy or move constructor, implementations are permitted to create a temporary object to hold the function parameter or result object. ... [ Note: This latitude is granted to allow objects of class type to be passed to or returned from functions in registers. — end note]
In your case, when I made both copy and move constructors defaulted:
S(const S &) = default;
S(S &&) = default;
assertion failed as well with GCC and Clang. Note that implicitly-defined constructors are trivial.

Related

Deprecated implicitely-declared copy constructor

I am trying to wrap my head around a weird characteristic of implicitely-declared copy constructors. Take the following example. Once the user implements a custom destructor, the copy-constructor isn't trivial anymore, but is still generated.
#include <type_traits>
#include <cstdio>
struct test {
test() = default;
~test() {
}
test(const test&) = default;
int i{42};
};
static_assert(std::is_copy_constructible_v<test>, "werks"); // OK
static_assert(std::is_trivially_copy_constructible_v<test>, "sad"); // FAILS
int main() {
test t;
test t2(t);
printf("%d\n", t2.i);
return 0;
}
Online version : https://godbolt.org/z/t-8_W3
My understanding of trivial constructors is they are generated by the compiler. However, cppreference states :
The generation of the implicitly-defined copy constructor is
deprecated if T has a user-defined destructor or user-defined copy
assignment operator.
So, it seems there is one more state an implicitly-declared constructor can be in, which is "deprecated". It is not "trivial" and not user implemented, but still generated by your compiler... Is that correct? Does someone know a type trait or workaround to verify if a constructor is "deprecated"?
is_trivially_copy_constructible<T> also requires that T be trivially destructible. What this trait checks is if the hypothetical variable definition:
T t(declval<T const&>());
would invoke no non-trivial operations. But test is not trivially destructible, and that destruction is implicit in that construction.
If you change ~test() { } to ~test() = default, the assertion will no longer trigger.
The note about deprecation of implicitly defined constructor is irrelevant, as you have an explicitly defaulted copy constructor.

Does the behavior of guaranteed copy elision depend on existence of user-defined copy constructor?

The following code behaves differently with or without user-defined copy constructor under GCC 8.0.1:
#include <cassert>
struct S {
int i;
int *p;
S() : i(0), p(&i) {}
// S(const S &s) : i(s.i), p(&i) {} // #1
// S(const S &s) : i(s.i), p(s.p) {} // #2
// S(const S &s) = delete; // #3
};
S make_S() {return S{};}
int main()
{
S s = make_S();
assert(s.p == &s.i);
}
With either of the commented user-defined copy constructors (even with #2, the one performing a simple shallow copy), the assertion will not fail, which means guaranteed copy elision works as expected.
However, without any user-defined copy constructor, the assertion fails, which means the object s in main function is not default-constructed. Why does this happen? Doesn't guaranteed copy elision perform here?
Quoting from C++17 Working Draft §15.2 Temporary Objects Paragraph 3 (https://timsong-cpp.github.io/cppwp/class.temporary#3):
When an object of class type X is passed to or returned from a function, if each copy constructor, move constructor, and destructor of X is either trivial or deleted, and X has at least one non-deleted copy or move constructor, implementations are permitted to create a temporary object to hold the function parameter or result object. ... [ Note: This latitude is granted to allow objects of class type to be passed to or returned from functions in registers. — end note]
In your case, when I made both copy and move constructors defaulted:
S(const S &) = default;
S(S &&) = default;
assertion failed as well with GCC and Clang. Note that implicitly-defined constructors are trivial.

Move constructor is automatic generated even a member doesn't have a move constructor?

Quoted from C++ Primer
if we explicitly ask the compiler to generate a move operation by
using = default, and the compiler is unable to move all the
members, then the move operation will be defined as deleted
the move constructor is defined as deleted if the
class has a member that defines its own copy constructor but does not also
define a move constructor, or if the class has a member that doesn’t define its
own copy operations and for which the compiler is unable to synthesize a move
constructor
Some code seems to violate this rule:
#include <utility>
#include <iostream>
struct X {
X() = default;
X(const X&) { std::cout << "X(const X&)" << std::endl; }
int i;
};
struct hasX {
hasX() = default;
hasX(const hasX &) = delete;
hasX(hasX &&) = default;
X mem;
};
int main()
{
hasX hx, hx2 = std::move(hx); //output is X(const X&)
}
X doesn't define move constructor and compiler can't synthesize one for it.
According to the rule above, hasX's move constructor is deleted.
However, because hasX's copy constructor is deleted, hx2 = std::move(hx) must call move constructor to output "X(const X&)", which shows hasX's move constructor is defined and it use X's copy constructor to "move". This seems to violate the rule above.
So, is't defined in C++ standard or just a compiler implementation?
Compiler I tested: VS2015 and a online compiler
Thank you for help!
Your book seems to be wrong. Copying is a valid move operation so as long as the member has a copy constructor in the form of const type&, so it can bind to rvalues, the move operation will fall back to a copy.
In your example
hasX(hasX &&) = default;
Can be replaced with
hasX(hasX &&rhs) : mem(std::move(rhs.mem)) {}
as that is what the default does and it will compile just fine.

Assignment from rvalue allowed when assignment operator explicitly deleted?

Consider the following code, which compiles under Clang, GCC, and VS 2015 (online example):
#include <utility>
class S
{
public:
S(int x) : i(x) { }
~S() { }
S(S&&) = default;
S(const S& ) = delete;
S& operator=(S&&) = delete;
S& operator=(const S&) = delete;
private:
int i;
};
S GetS()
{
// This is a contrived example with details elided. Assume
// there's a reason in the actual use case for returning via
// std::move.
return std::move( S(999) );
}
int main()
{
S tmp = GetS(); // <-- Assignment allowed even though assignment operator is deleted?
return 1;
}
I'm unclear as to why the line
S tmp = GetS();
compiles, executing the move constructor instead of the move assignment operator.
I know that RVO allows construction and assignment to be elided as an optimization, but it was my understanding that explicitly deleting an operator should cause compilation to fail if that operator is explicitly used in the code.
Is there some clause in the C++11 specification that allows a compiler to transform assignment initialization into copy construction, even when assignment operators for a type have been explicitly deleted?
That's because this isn't assignment:
S tmp = GetS();
it's called copy-initialization and invokes the move constructor, which you have explicitly defaulted.
Basically, an assignment operator is only invoked on an object that already exists. tmp doesn't exist yet, this statement initializes it. As such, you're calling a constructor.
Note that what happens inside of GetS() does not affect the rules for how tmp is constructed. GetS() is an rvalue either way.

How to add elements to vector without invoking copy-constructor?

For some reason my temporary local object is always copy-constructed/destroyed when added to a vector which is causing problems due to nested std::reference_wrapper which are getting invalid because of the copy-construction and destruction afterwards (std::reference_wrapper targets are inside the object which gets destroyed -> so they are invalid in the copy-constructed object, if the source object is destroyed). But if possible I want to avoid the additional copying / destroying completely - that seems impossible, because whatever I tried always it wants to invoke the copy constructor (even using std::vector::emplace_back).
Considering this simple example (for easier understanding without std::reference_wrapper involved), it always tries to invoke the copy-constructor - I don't get why.
#include <vector>
class A{
public:
A(int a) : a(a){ }
int getInt() const{ return a; }
A(const A&) = delete; /* to deny copy-construction */
private:
int a;
};
int main(int argc, char* argv[]){
std::vector<A> vec;
vec.emplace_back(3); /* tries to call copy constructor */
vec.push_back(A(3)); /* tries to call copy constructor */
vec.push_back(std::move(A(3))); /* tries to call copy constructor */
return 0;
}
Any ideas what I'm missing here?
Per the Visual Studio 2013 documentation, emphasis mine:
"Rvalue references v3.0" adds new rules to automatically generate move constructors and move assignment operators under certain conditions. However, this is not implemented in Visual C++ in Visual Studio 2013, due to time and resource constraints.
Visual Studio 2013 is specified as using Rvalue references v2.1.
Note: As T.C. notes in the comments there is also an issue with explicitly disabling the copy constructor in your example. Per cppreference.com.
If no user-defined move constructors are provided for a class type (struct, class, or union), and all of the following is true:
there are no user-declared copy constructors
there are no user-declared copy assignment operators
there are no user-declared move assignment operators
there are no user-declared destructors
(until C++14) the implicitly-declared move constructor is not defined as deleted due to conditions detailed in the next section
then the compiler will declare a move constructor as an inline public member of its class with the signature T::T(T&&).
A class can have multiple move constructors, e.g. both T::T(const T&&) and T::T(T&&). If some user-defined move constructors are present, the user may still force the generation of the implicitly declared move constructor with the keyword default.
Which means your example code also prevents auto generation of the move constructor (i.e., it has a user-declared copy constructor).
You need to explicitly declare your move constructor and/or move assignment operator. The following works for your example.
class A
{
public:
A(int a) : a(a) {}
A(const A&) = delete;
A(A&& other) : a(other.a) {}
int getInt() const { return a; }
private:
int a;
};
int main(int argc, char* argv[])
{
std::vector<A> vec;
vec.emplace_back(3);
vec.push_back(A(3));
vec.push_back(std::move(A(3)));
return 0;
}