constexpr if and the return value optimization - c++

I have this code:
#include <string>
class A {
public:
// A(A const &) = delete; // Code fails if this is uncommented.
explicit A(int);
explicit A(::std::string const &);
private:
::std::string myname_;
int foo_;
};
static constexpr bool which = false;
A test(::std::string const &s, int a)
{
if constexpr (which) {
A x{a};
return x;
} else {
A y{s};
return y;
}
}
This code fails if A has a deleted copy constructor. But, given the rules about return type for functions with if constexpr in them, it seems like the compiler should be applying RVO here.
Is there a reason why it isn't, other than it being an overlooked case in the language specification?

This has nothing to do with if constexpr
Simply this code is not allowed to compile:
class A {
public:
A(A const &) = delete;
explicit A(int);
};
A test(int a)
{
A x{a};
return x; // <-- error call to a deleted constructor `A(A const &) = delete;`
}
The changes in C++17 you are thinking of have to do with temporary materialization and don't apply to NRVO because x is not a prvalue.
For instance this code was illegal pre C++17 and now it is allowed:
A test(int a)
{
return A{a}; // legal since C++17
}

This is NRVO, which is non-mandatory copy elision:
(emphasis mine)
In a return statement, when the operand is the name of a non-volatile object with automatic storage duration, which isn't a
function parameter or a catch clause parameter, and which is of the
same class type (ignoring cv-qualification) as the function return
type. This variant of copy elision is known as NRVO, "named return
value optimization".
This is an optimization: even when it takes place and the copy/move
(since C++11) constructor is not called, it still must be present and
accessible (as if no optimization happened at all), otherwise the
program is ill-formed:
BTW: Note that in your code, both the if part and else part of the constexpr if statement will be checked.
Outside a template, a discarded statement is fully checked. if
constexpr is not a substitute for the #if preprocessing directive:
void f() {
if constexpr(false) {
int i = 0;
int *p = i; // Error even though in discarded statement
}
}

Related

What is the rule for type deduction for assignment using brace initialization?

In the following code, to my surprise, the { } appears to create a default initialized instance of int rather than of A.
struct A {
int i = 1;
A() = default;
A& operator=(const A& a) {
this->i = a.i;
return *this;
}
A& operator=(int i) {
this->i = i;
return *this;
}
};
int main() {
A a;
a = { };
return a.i;
}
What rule is used to parse a = { }? I thought that a = { } would have to be equivalent to a = decltype(a){ }.
What is the right term for the type of initialization that takes place here?
Here's an example on godbolt. At -O0 the assembly shows exactly which function is being used. Unless the int assignment operator is removed, A::operator=(int) is called.
Firstly, the braced-init-list {} could be used to initialize both int and A, which are also considered as conversion, and the conversion from {} to int wins in overload resolution, since the conversion from {} to A (via A::A()) is considered as a user-defined conversion.
Assignment operator is not special, compilers just collect all the viable operator=s in overload set, then determine which one should be selected by overload resolution. If you add another operator=(float) you'll get an ambiguous error since conversion from {} to int and from {} to float are both considered as standard conversion with same rank, which wins against user-defined conversion.

constexpr and mutable member and implicit copy-ctor

The following code compiles in clang 7+, but not in 5 & 6 (with c++17 and c++14).
The problem for clang 5 and 6 seems to be that the implicit copy ctor reads from the mutable member x.
Can anybody tell me, if the whole construction is standard-compliant (c++17), or if the program is ill-formed? Or was there a change in the standard regarding the implicit copy-ctor which might not be implemented in the earlier clang versions?
struct Foo {
int a;
mutable int x{};
constexpr Foo() : a(0) {}
//constexpr Foo(const Foo& other) : a(other.a) {} // <- with this line it works on Clang 5 & 6, too
};
struct FooFactory {
static constexpr auto create() {
auto f = Foo{};
return f;
}
};
int main() {
constexpr Foo f = FooFactory::create();
++f.x;
}
Live code here.
This is a well-formed C++17 program. A constexpr function can of course read (and write) non-constant variables:
constexpr int f(int i) {
int j=i;
++j;
return i+j; // neither is a constant expression
}
The rule is that anything examined in a constant expression must either be a constant or have its lifetime begun during the expression’s evaluation. In your case, the lifetime of create’s f.x plainly begins within the evaluation of the constant expression that is the initialization of main’s f. It is true, however, that no Foo object can be copied by a constant expression that does not also create that object, whether or not it is constexpr.
The only other candidate problem is if the copy constructor were not constexpr, but those requirements are very weak. The only relevant ones are that every (non-variant) member be initialized, which is certainly satisfied, and that it be usable in at least one constant expression, which has been demonstrated.
The code is ill-formed. For the reference, here is simplified code, which fails on all compilers:
struct Foo {
int a;
mutable int x{};
constexpr Foo() : a(0) {}
};
int main() {
constexpr Foo f;
constexpr Foo f1 = f;
}
As per [expr.const] 7.7,
A variable is usable in constant expressions after its initializing
declaration is encountered if
it is a constexpr variable,
or ... (3.5) a non-mutable
subobject or reference member of any of the above.
This disqualifies default copy constructors.

move constructor when returning a "chained" object

Say I have a class that is move-only, and this class has methods that "chain". example:
struct C {
C() = default;
C(C const&) = delete;
C(C&&) = default;
C& chained() {
return *this;
}
int a;
};
C foo(C c) {
return c.chained();
}
int main()
{
auto o = foo(C{});
}
I get an error at the return statement of foo: "Use of deleted function 'C::C(const C&)'".
Why is trying to call the copy constructor? Shouldn't it be using the move constructor since its a return statement?
Why is trying to call the copy constructor? Shouldn't it be using the move constructor since its a return statement?
No. You are referring to, and misinterpreting, copy elision (as pertaining to NRVO). Only when the returned expression is a an id-expression (just a name) that refers to an object from the function's parameter list or a local, will a move be attempted first.
You don't return an id-expression, your expression is the result of a call to a member function.

C++17 make_optional constexpr-ness

This page says that the make_optional function in C++17 returns a constexpr optional<...>. I think (I might be wrong though) this would require that optional<T> has a constexpr copy or move constructor. However, this page also says that's not the case.
I don't know how make_optional can be implemented as the C++1z draft currently stands. See this post for clarification. Is there some workaround, or maybe it's just the standard draft/cppreference's mistake?
Thanks to #Yakk and #T.C. for their explanations. I feel an example should make things clearer:
struct wrapper {
int value;
// non-explicit constexpr constructor
constexpr wrapper(int v) noexcept : value(v) {}
// non-constexpr copy & move constructors
wrapper(const wrapper& that) noexcept : value(that.value) {}
wrapper(wrapper&& that) noexcept : value(that.value) {}
};
constexpr wrapper make_wrapper(int v)
{
return {v};
}
int main()
{
constexpr auto x = make_wrapper(123); // error! copy/move construction,
// but no constexpr copy/move ctor
constexpr int y = make_wrapper(123).value; // ok
static_assert(y == 123, ""); // passed
}
So make_wrapper does successfully return a constexpr wrapper; it's the copy/move construction (although usually elided by compilers) that prevents the code from compiling, since there is no constexpr copy/move constructor.
We can verify the constexpr-ness of the returned (temporary) wrapper object by using its member value to initialize a constexpr variable.
You can directly construct return values in C++11 with return {something}; If there are any non-explicit ctors that are constexpr you can return it from a function.

returning constant object and construction from temporary

I've recently discovered difference between msvc and g++/clang++ compilers which is related to the behavior of RVO in case where a constant object is returned. A simple example which illustrates the difference:
#include <iostream>
class T
{
public:
T() { std::cout << "T::T()\n"; }
~T() { std::cout << "T::~T()\n"; }
T(const T &t) { std::cout << "T::T(const T&)\n"; }
T(T &&t) { std::cout << "T::T(T&&)\n"; }
T(const T &&t) { std::cout << "T::T(const T&&)\n"; }
};
const T getT()
{
T tmp;
return tmp;
}
int main()
{
T nonconst = getT();
}
With optimizations enabled both examples will produce only T() and ~T() calls, which is expected due to RVO (which by the way ignores constness of returned type). But without them results differ.
clang++ or g++ with -fno-elide-constructors everything by the rules:
T::T()
T::T(T&&) // from non-const local tmp variable to temp storage (const, due to return-type)
T::~T()
T::T(const T&&) // from constant temp storage to nonconst variable
T::~T()
T::~T()
msvc (2013) is ignoring return-type constness:
T::T()
T::T(T&&) // from local non-const tmp var to non-const nonconst var
T::~T()
T::~T()
With slight modification:
const T getT()
{
const T tmp; // here const is added
return tmp;
}
clang++ or g++ with -fno-elide-constructors, everything as expected again:
T::T()
T::T(const T&&) // from const local tmp var to temp storage (const, due to return-type)
T::~T()
T::T(const T&&) // from constant temp storage to nonconst variable
T::~T()
T::~T()
msvc (2013):
T::T()
T::T(const T&&) // from local const tmp var to non-const nonconst var
T::~T()
T::~T()
All that explains next problem in the original version (without const for tmp): if construction from constant temporary is prohibited like T(const T &&t) = delete; g++/clang++ produce error: use of deleted function ‘T::T(const T&&)’ and msvc does not.
So, is that a bug in MSVC? (it ignores return-type specification and breaks suggested semantic)
In short: msvc compiles the following code, g++/clang++ don't.
#include <iostream>
class T
{
public:
T() { std::cout << "T::T()\n"; }
~T() { std::cout << "T::~T()\n"; }
T(const T &t) { std::cout << "T::T(const T&)\n"; }
T(T &&t) { std::cout << "T::T(T&&)\n"; }
T(const T &&t) = delete;
};
const T getT()
{
const T tmp;
return tmp;
}
int main()
{
T nonconst = getT(); // error in gcc/clang; good for msvc
}
I believe the const is a red herring here. We could simplify the example down to:
struct T
{
T() = default;
T(T &&) = delete;
};
T getT()
{
T tmp;
return tmp;
}
int main()
{
T x = getT();
}
This fails to compile on gcc or clang, and I believe that failure is correct. Regardless of whether copy elision happens, we still do overload resolution on the constructor. From [class.copy]:
When the criteria for elision of a copy/move operation are met, but not for an exception-declaration, and the
object to be copied is designated by an lvalue, or when the expression in a return statement is a (possibly
parenthesized) id-expression that names an object with automatic storage duration declared in the body or
parameter-declaration-clause of the innermost enclosing function or lambda-expression, overload resolution
to select the constructor for the copy is first performed as if the object were designated by an rvalue. If
the first overload resolution fails or was not performed, or if the type of the first parameter of the selected
constructor is not an rvalue reference to the object’s type (possibly cv-qualified), overload resolution is
performed again, considering the object as an lvalue. [ Note: This two-stage overload resolution must be
performed regardless of whether copy elision will occur. It determines the constructor to be called if elision
is not performed, and the selected constructor must be accessible even if the call is elided. —end note ]
Following the rules, we perform overload resolution as if the object were an rvalue. That overload resolution finds T(T&& ), which is explicitly deleted. As that call is ill-formed, the entire expression is ill-formed.
Copy/move elision is merely an optimization. The code that it would elide has to be valid to begin with.