Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does the standard state that copies must be equivalent?

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?

like image 677
Barry Avatar asked Jan 23 '17 22:01

Barry


1 Answers

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.

C++14 §12.8/31 class.copy/31:

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.

like image 172
Cheers and hth. - Alf Avatar answered Nov 15 '22 17:11

Cheers and hth. - Alf