7

I've found strange behavior of a code which is apparently ignoring const-ness:

#include <iostream>

using std::cerr;

class A
{
public:
    A() { cerr << "A::A()\n"; }
    A(const A &a) { cerr << "A::A(const A&)\n"; }
    A(A &&) { cerr << "A::A(A&&)\n"; }
    A & operator = (const A &a) { cerr << "A::operator=(const A&)\n"; return *this; }
    A & operator = (A &&a) { cerr << "A::operator(A&&)\n"; return *this; }
    ~A() { cerr << "A::~A()\n"; }

    const A get() const { cerr << "const A A::get() const\n"; return A(); }
    A get() { cerr << "A A::get()\n"; return A(); }
};

int main()
{
    const A a;
    A b = a.get();
}

Firstly, what I did expect here: a is a constant, so the constant-version of get() is invoked. Next, constant object is being returned, but on the left side is non-constant object b, so the copy-constructor is ought to be called. Which is not:

A::A()
const A A::get() const
A::A()
A::~A()
A::~A()

Is this behavior expected by c++ standard? So, is it okay that constness of a temporary object is simply ignored by RVO? And how copying could be enforced here?

Output with copy-elision disabled (-fno-elide-constructors) makes an additional move and the expected copy-constructor call:

A::A()
const A A::light_copy() const
A::A()
A::A(A&&)
A::~A()
A::A(const A&)
A::~A()
A::~A()
A::~A()

If a object is not constant, then it will be two moves without copying, which is expected too.

PS. The behavior matters for me because the one I see is breaking shallow-copying const-strictness: for const-version of get() (which is shallow_copy() actually) I need to be sure that no modification of the returned object will be made, because the returned object is a shallow copy and a modification on the shallow copy will affect the "parent" object (which might be a constant).

Alexander Sergeyev
  • 922
  • 10
  • 19
  • 2
    Return value optimisation (RVO) in action. – Jarod42 Sep 03 '15 at 08:19
  • BTW, see [should-i-still-return-const-objects-in-c11](http://stackoverflow.com/questions/13099942/should-i-still-return-const-objects-in-c11) – Jarod42 Sep 03 '15 at 08:23
  • With "light_copy", do you mean 'shallow copy'? –  Sep 03 '15 at 08:37
  • @Tive Yes, that is precisely what I meant – Alexander Sergeyev Sep 03 '15 at 08:38
  • 1
    Are you sure you want to return newly created object `A()` instead of `*this` in `get` methods? If so, these methods can be `static`. However in that case, only one of them is accepted by the compiler. – Melebius Sep 03 '15 at 08:38
  • 1
    T.C.'s answer tells you that the behavior you're seeing is mandated by the standard. I suspect you're mixing concepts up. A change to a shallow copy will *always* be 'propagated' to the 'source' of the shallow copy. Now it sounds like you want a shallow copy until the shallow copy is changed. That would be copy-on-write or the handle idiom. Note that copy-on-write is hard to get right and performs badly in a multi-threaded world (which is why the standard no longer permits std::string objects to be implemented with CoW). –  Sep 03 '15 at 09:23

2 Answers2

7

So, is it okay that constness of a temporary object is simply ignored by RVO?

Yes. [class.copy]/p31 (quoting N4527, which incorporates some DRs that clarifies the intent; emphasis mine):

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 nonvolatile automatic object (other than a function parameter or a variable introduced by the exception-declaration of a handler (15.3)) with the same type (ignoring cv-qualification) as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value
  • [...]
  • when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same type (ignoring cv-qualification), the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move
  • [...]

The third bullet is the one applicable here; note that a similar rule applies to NRVO (first bullet) as well.

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • Thanks for the clarification; any ideas how copying could be enforced here? – Alexander Sergeyev Sep 03 '15 at 08:45
  • @AlexanderSergeev What exactly are you hoping to achieve by copying here? Your functions return *new* objects; if you enforced copying, you'd be copying a new object into another new object, then throwing away the first object. The reason the standard permits copy elision is to avoid creating and discarding superfluous objects; it should have no effect on the actual behavior of the code. – Kyle Strand Sep 03 '15 at 18:21
  • @KyleStrand All of this is actually needed for me to implement `shallow_copy`: if the parent object is constant, I need to make sure that const-overload of `shallow_copy` will return constant object. And copy-constructor creates a deep copy. So, the point is that `const A parent; const A copy = parent.shallow_copy();` works as expected, and `const A parent; A copy = parent.shallow_copy();` uses additional copy constructor (because the return type is constant and the left side is not) and hence makes a deep copy. But it is not working because NVRO bypasses const'ness restriction. – Alexander Sergeyev Sep 03 '15 at 18:29
  • @AlexanderSergeev If you have a handle to some data that must be cleaned up by the destructor, then `const` won't help you, because destructors can't be CV-qualified. I think what you might need here is to return an rvalue-qualified instance, not a const-qualified instance. – Kyle Strand Sep 03 '15 at 18:56
  • 2
    @AlexanderSergeev Work with the language, not against it. If you want a constant view of an `A`, make an `A_view` class that provides only a constant view. – T.C. Sep 03 '15 at 19:01
1

If you want to forbid construction/assignation from const temporary, you may mark as deleted these methods:

A(const A &&) = delete;
A& operator = (const A &&) = delete;

Live Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302