user11740831
user11740831

Reputation:

Efficiency of string reference parameters in C++

In C++ when arguments are strings, is there any noticeable speed/efficiency gained by making them references to string objects just like is done when arguments are objects of a class created by the programmer? For example, if I had a class called Person that had a first and last name, I could do for one of the constructors:

Person(const string& firstName, const string& lastName);

vs.

Person(string firstName, string lastName);

Is the first one more efficient, or is the efficiency gained so trivial it doesn't matter?

Upvotes: 0

Views: 659

Answers (3)

Vlad from Moscow
Vlad from Moscow

Reputation: 310930

Instead of these two approaches

Person(const string& firstName, const string& lastName);

and

Person(string firstName, string lastName);

a more efficient approach is when you have the following two constructors together

Person(const string &firstName, const string &lastName);
Person( string &&firstName, string &&lastName );

This approach

Person(const string& firstName, const string& lastName);

is less efficient when the arguments are rvalues.

This approach

Person(string firstName, string lastName);

is less efficient when the arguments are lvalues.

Here are two demonstrative program that show differences between the three approaches.

#include <iostream>

struct A
{
    static int value;

    explicit A() : x( ++value )
    {
        std::cout << "explicit A(), x = " << x << "\n";
    }

    A( const A &a ) noexcept : x( ++value )
    {
        std::cout << "A( const A & ), x = " << x << "\n";
    }

    A( A &&a ) noexcept : x( ++value )
    {
        std::cout << "A( A && ), x = " << x << "\n";
    }

    ~A()
    {
        std::cout << "~A(), x = " << x << "\n";
    }

    int x;
};

int A::value = 0;

struct B
{
    B( const A &a1, const A &a2 ) : a1( a1 ), a2( a2 )
    {
        std::cout << "B( const A &, const A & )\n";
    }
    A a1, a2;
};

struct C
{
    C( A a1, A a2 ) : a1( std::move( a1 ) ), a2( std::move( a2 ) )
    {
        std::cout << "C( A, A )\n";
    }

    A a1, a2;
};

struct D
{

    D( const A &a1, const A &a2 ) : a1( a1 ), a2( a2 )
    {
        std::cout << "D( const A &, const A & )\n";
    }

    D( A &&a1, A &&a2 ) : a1( std::move( a1 ) ), a2( std::move( a2 ) )
    {
        std::cout << "D( A &&, A && )\n";
    }

    A a1, a2;
};

int main()
{
    A a1;
    A a2;

    std::cout << '\n';

    B b( a1, a2 );

    std::cout << "b.a1.x = " << b.a1.x << ", b.a2.x = " << b.a2.x << '\n';

    std::cout << '\n';

    C c( a1, a2 );

    std::cout << "c.a1.x = " << c.a1.x << ", c.a2.x = " << c.a2.x << '\n';

    std::cout << '\n';

    D d( a1, a2 );

    std::cout << "d.a1.x = " << d.a1.x << ", d.a2.x = " << d.a2.x << '\n';

    std::cout << '\n';

    return 0;
}

The program output is

explicit A(), x = 1
explicit A(), x = 2

A( const A & ), x = 3
A( const A & ), x = 4
B( const A &, const A & )
b.a1.x = 3, b.a2.x = 4

A( const A & ), x = 5
A( const A & ), x = 6
A( A && ), x = 7
A( A && ), x = 8
C( A, A )
~A(), x = 6
~A(), x = 5
c.a1.x = 7, c.a2.x = 8

A( const A & ), x = 9
A( const A & ), x = 10
D( const A &, const A & )
d.a1.x = 9, d.a2.x = 10

~A(), x = 10
~A(), x = 9
~A(), x = 8
~A(), x = 7
~A(), x = 4
~A(), x = 3
~A(), x = 2
~A(), x = 1

And

#include <iostream>

struct A
{
    static int value;

    explicit A() : x( ++value )
    {
        std::cout << "explicit A(), x = " << x << "\n";
    }

    A( const A &a ) noexcept : x( ++value )
    {
        std::cout << "A( const A & ), x = " << x << "\n";
    }

    A( A &&a ) noexcept : x( ++value )
    {
        std::cout << "A( A && ), x = " << x << "\n";
    }

    ~A()
    {
        std::cout << "~A(), x = " << x << "\n";
    }

    int x;
};

int A::value = 0;

struct B
{
    B( const A &a1, const A &a2 ) : a1( a1 ), a2( a2 )
    {
        std::cout << "B( const A &, const A & )\n";
    }
    A a1, a2;
};

struct C
{
    C( A a1, A a2 ) : a1( std::move( a1 ) ), a2( std::move( a2 ) )
    {
        std::cout << "C( A, A )\n";
    }

    A a1, a2;
};

struct D
{

    D( const A &a1, const A &a2 ) : a1( a1 ), a2( a2 )
    {
        std::cout << "D( const A &, const A & )\n";
    }

    D( A &&a1, A &&a2 ) : a1( std::move( a1 ) ), a2( std::move( a2 ) )
    {
        std::cout << "D( A &&, A && )\n";
    }

    A a1, a2;
};

int main()
{
    B b( A{}, A{} );

    std::cout << "b.a1.x = " << b.a1.x << ", b.a2.x = " << b.a2.x << '\n';

    std::cout << '\n';

    C c( A{}, A{} );

    std::cout << "c.a1.x = " << c.a1.x << ", c.a2.x = " << c.a2.x << '\n';

    std::cout << '\n';

    D d( A{}, A{} );

    std::cout << "d.a1.x = " << d.a1.x << ", d.a2.x = " << d.a2.x << '\n';

    std::cout << '\n';

    return 0;
}

The program output is

explicit A(), x = 1
explicit A(), x = 2
A( const A & ), x = 3
A( const A & ), x = 4
B( const A &, const A & )
~A(), x = 2
~A(), x = 1
b.a1.x = 3, b.a2.x = 4

explicit A(), x = 5
explicit A(), x = 6
A( A && ), x = 7
A( A && ), x = 8
C( A, A )
~A(), x = 6
~A(), x = 5
c.a1.x = 7, c.a2.x = 8

explicit A(), x = 9
explicit A(), x = 10
A( A && ), x = 11
A( A && ), x = 12
D( A &&, A && )
~A(), x = 10
~A(), x = 9
d.a1.x = 11, d.a2.x = 12

~A(), x = 12
~A(), x = 11
~A(), x = 8
~A(), x = 7
~A(), x = 4
~A(), x = 3

It is seen that in the both programs the class D behaves more efficiently compared with the classes B and C.

Upvotes: 1

Arie Charfnadel
Arie Charfnadel

Reputation: 158

Well, You made me recall a good rule of thumb one of my professors taught me once.

When you have to decide if to pass an object to a function by value or by reference: if the object is larger than the reference to it, up to 4x - we will pass the object itself, if it's larger than 4x - then we will pass a reference to it.

Of course, it's under the assumption that in either way we are not planning to change the object.

Upvotes: 0

eerorika
eerorika

Reputation: 238301

Is the first one more efficient,

It can be, potentially. But not necessarily.

or is the efficiency gained so trivial it doesn't matter?

It depends on many things. If the strings are short (enough to fit within the small string optimisation of the target system), then the difference will likely be small either way.

If you are not going to store a copy of the string, and if the string is large, then reference will likely be significantly faster.

If you are going to store a copy, and are passing an rvalue to the function, and the string is large, then the non-reference will likely be significantly faster because you can create the copy by moving.


Regardless, whether the difference between these alternatives is significant in your program, can be found only by measuring.

Upvotes: 1

Related Questions