Home > database >  c language or compiler facility to gaurantee copy elision
c language or compiler facility to gaurantee copy elision

Time:11-16

As mentioned in cppreference: https://en.cppreference.com/w/cpp/language/copy_elision

it is guaranteed in c 17 that copy elision must be applied for some cases like

SomeBigObject SomeBigObject::Factory(...) {
     SomeBigObject local;
     ...
     return local;
}

(example taken from https://abseil.io/tips/11 )

but not for cases like

SomeBigObject SomeBigObject::Factory(...) {
     SomeBigObject local1;
     SomeBigObject local2;
     ...

     if (cond_1) {
         return local1;
     } else {
         return local2;
     }
}

For a c user, it is somewhat difficult to be 100% sure that copy elision / NVO is applied to the functions' return value above.

So, to be sure that a object would not be copied, we usually write something like

     SomeBigObject obj;
     func(&obj);

whereas in most cases a one-liner like

     SomeBigObject obj = func();

would suffice.

Is there any language/compiler facility to help us guarantee that at compile time (maybe some kind of static_assert()) so that we can be confident writing those one-liners ?

I know that marking the copy constructor deleted would do the good, but copy is needed in some cases.

CodePudding user response:

it is guaranteed in c 17 that copy elision must be applied for some cases like

SomeBigObject SomeBigObject::Factory(...) {
   SomeBigObject local;
   ...
   return local;
}

That is not correct. Gauranteed copy elision only applies to cases where the object returned is created in the return statement like

SomeBigObject SomeBigObject::Factory(...) {
    // code
    return SomeBigObject(some_intializer);
    // or
    return {some_intializer};
}

Both code blocks in your question are examples of NRVO (Named Return Value Optimization) and there is no guarantee that NRVO will be applied, only that it is allowed to be applied.

If your object is moveable, and moving is a cheap operation, then you don't need to do anything as all function local objects are treated as rvalues in the return statement and moved if possible.

If your object doesn't have a cheap move operation then the next best thing you can do to avoid expensive copies is to use a smart pointer like

std::unique_ptr<SomeBigObject> SomeBigObject::Factory(...) {
   auto local = std::make_unique<SomeBigObject>();
   // code
   return local;
}

and all that costs on return is a pointer swap.

CodePudding user response:

Your first example does not guarantee copy elision either. Both the first and second example may elide the copy, but it is up to the compiler. However, in the first case it is much simpler for the compiler to recognize the pattern and apply the optimization. So you are likely to see it applied in the first case, but less likely in the second case where it may be impossible for the compiler to track which of the two local objects should be constructed as the result object, although it may still be possible depending on the exact circumstances.

Using the

 SomeBigObject obj;
 func(&obj);

form doesn't fix that issue either though. The function parameter can be easily identified with only one of the two localX objects, which is precisely why it is generally harder for a compiler to do the optimization for such cases.

Also note that the article you link was written before C 17 when some copy elision became mandatory for the first time. Before C 17 it was all up to the compiler. The article just reflects the behavior of typical implementations (at the time?).

The rule now is pretty simple: If you initialize an object from a prvalue of the same type (up to cv-qualifiers), then copy elision is guaranteed. This applies to chained initialization through multiple prvalues and also function calls as well.

func() is a prvalue of type SomeBigObject, so in SomeBigObject obj = func(); copy elision is mandatory in as far as there will be no object as the return value of the function distinct from obj. (since C 17)

In both your function implementations the return operand is however a lvalue and so it isn't guaranteed that obj and local/localX will be identified with one-another. Under certain circumstances satisfied here, the compiler may still do this identification and eliminate the copy/move. But that is always non-mandatory.

So ideally you want to not return a local variable, but construct the return value directly in the return statement if the logic of the function allows that. Or at least try to scope each local variable used as a return operand so that all return statements in its scope return only this variable.

I am not aware of any tool for the common compilers or in the language to statically check at compile-time whether or not the non-mandatory named return value optimization is applied to a function.

CodePudding user response:

Regardless of whether copy elision is guaranteed or not, if you really need to guarantee that the copy constructor isnt called you could use a wrapper class as follows:

template<typename T>
struct NoCopy
{
    NoCopy() = default;
    
    template<typename ...Args>
    NoCopy(Args&&... args):
    value(std::forward<Args>(args)...)
    {}

    NoCopy(NoCopy&&) = default;

    NoCopy(NoCopy&) = delete;
    NoCopy(const NoCopy&) = delete;

    T value;
};

The wrapper cannot be copied, but its member can

NoCopy<SomeBigObject> obj(arguments...);
auto cpy = obj; //error
auto mv = std::move(obj); // OK
auto cpyMember = obj.value; // OK
  • Related