Suppose I have a weird string type, that either owns or doesn't own it's underlying buffer:
class WeirdString {
private:
char* buffer;
size_t length;
size_t capacity;
bool owns;
public:
// Non-owning constructor
WeirdString(char* buffer, size_t length, size_t capacity)
: buffer(buffer), length(length), capacity(capacity), owns(false)
{ }
// Make an owning copy
WeirdString(WeirdString const& rhs)
: buffer(new char[rhs.capacity])
, length(rhs.length)
, capacity(rhs.capacity)
, owns(true)
{
memcpy(buffer, rhs.buffer, length);
}
~WeirdString() {
if (owns) delete [] buffer;
}
};
Does that copy constructor violate the standard somewhere? Consider:
WeirdString get(); // this returns non-owning string
const auto s = WeirdString(get());
s
is either owning or non-owning depending on whether or not the additional copy constructor got elided, which in C++14 and earlier is permitted but optional (though in C++17 is guaranteed). That Schrödinger's ownership model suggests that this copy constructor is, in itself, undefined behavior.
Is it?
A more illustrative example might be:
struct X {
int i;
X(int i)
: i(i)
{ }
X(X const& rhs)
: i(rhs.i + 1)
{ } ~~~~
};
X getX();
const auto x = X(getX());
Depending on which copies get elided, x.i
could be 0, 1, or 2 more than whatever was returned in getX()
. Does the standard say anything about this?
Regarding the new question's code
struct X {
int i;
X(int i)
: i(i)
{ }
X(X const& rhs)
: i(rhs.i + 1)
{ } ~~~~
};
X getX();
const auto x = X(getX());
Here the copy constructor doesn't copy, so you're breaking the compiler's assumption that it does.
With C++17 I believe you're guaranteed that it's not invoked in the above example. However I don't have a draft of C++17 at hand.
With C++14 and earlier it's up to the compiler whether the copy constructor is invoked for the call of getX
, and whether it's invoked for the copy initialization.
” When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects.
This is not undefined behavior in the sense of the formal meaning of that term, where it can invoke nasal demons. For the formal terminology I'd choose unspecified behavior, because that's behavior that depends on the implementation and not required to be documented. But as I see it what name one chooses doesn't really matter: what matters is that the standard just says that under the specified conditions a compiler can optimize a copy/move construction, regardless of the side effects of the optimized-away constructor – which you therefore can not and should not rely on.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With