Grapes
Grapes

Reputation: 2573

Pass-by-value/const correctness with C++11

Before C++11, I would write my functions like so:

struct Color {
    float R,G,B,A;
    Color(float NewR,float NewG,float NewB,float NewA):
        R(NewR),G(NewG),B(NewB),A(NewA)
};
struct Item {
    Color color;

    void SetColor(const Color& new_color)
    {
        color=new_color; // Copy is created
    }
};

And then call it like so:

a_item.SetColor(Color(1.0f,1.0f,1.0f,1.0f));

Color new_color(1.0f,0.0f,0.0f,1.0f); // alternative way 
a_item.SetColor(new_color);

Now with C++11, I write my functions like this when applicable:

struct Item {
    Color color;

    void SetColor(Color new_color)
    {
        color=std::move(new_color); // No extra copy is created
    }
};

And then call it like so:

a_item.SetColor(Color(1.0f,1.0f,1.0f,1.0f)); // already Rvalue

Color new_color(1.0f,0.0f,0.0f,1.0f); // alternative way 
a_item.SetColor(std::move(new_color));

I was wondering if this was a good practice. It seems that when an object ownership is transferred, my functions become pass-by-value with move semantics.

Speaking of const correctness, should my constructor be defined like this:

    Color(const float NewR,const float NewG,const float NewB,const float NewA):
        R(NewR),G(NewG),B(NewB),A(NewA)

And my SetColor function defined like this?:

    void SetColor(const Color new_color)
    {
        color=std::move(new_color); // No extra copy is created
    }

Upvotes: 1

Views: 412

Answers (2)

Casey
Casey

Reputation: 42554

I'll throw the "perfect forwarding setter" technique into the mix:

struct Item {
    Color color;

    template <typename T>
    void SetColor(T&& new_color)
    {
        color=std::forward<T>(new_color);
    }
};

This will move from rvalues, copy from lvalues, and even accepts other types that are convertible to Color.

Upvotes: 1

Daniel Frey
Daniel Frey

Reputation: 56863

Regarding the first part, this seems basically fine, but read to the end of this answer. For the second part, you should first know that top-level const is stripped from the function's signature. So this

void foo( Color c );

is identical to

void foo( const Color c );

For the caller, it therefore makes no difference. The only difference is when you define the function, if you put const to a parameter for the definition, the compiler won't modify it. This doesn't make sense in the context you are asking the question: If you want to move the instance somewhere else, it is not const.

If you think const is the right way to go, use a const reference. Now back to your first question: Using void f( Color c ); seems easy and in some cases, superfluous copies are avoided. The problem is that this does not work in all cases and on todays compilers. Compared to overloading const Color& and Color&&, sometimes an additional move is generated. The only benefit is that you need fewer overloads, which might become important once you have multiple parameters.

To explain the difference with code:

void X::f( Color c )
{
   this->c = std::move(c); // 1 move=
}

void X::g( const Color& c )
{
   this->c = c; // 1 copy=
}

void X::g( Color&& c )
{
   this->c = std::move(c); // 1 move=
}

X x;
Color c;
x.f(c); // 1 copy-ctor, 1 move=
x.g(c); // 1 copy=
x.f(Color()); // 1 ctor, 1 move=
x.g(Color()); // 1 ctor, 1 move=
x.f(std::move(c)); // 1 move-ctor, 1 move=
x.g(std::move(c)); // 1 move=

As you can see, the "traditional" way of using a const reference combined with a C++11 rvalue-reference overload has advantages wrt the number of move-operations (which still do have a cost). Balance it with the simplification of not having overloads and judge for yourself.

Upvotes: 4

Related Questions