0

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?

  • ***or is the efficiency gained so trivial it doesn't matter?*** if you are looking for a measurable difference it likely depends on how many hundreds of times this function will be called. – drescherjm Jun 16 '20 at 12:25
  • The first one is, obviously, slightly more efficient. Passing an address of an object is less work than making a copy of it. – Sam Varshavchik Jun 16 '20 at 12:25
  • There is no difference between the standard library classes and classes you create yourself. – molbdnilo Jun 16 '20 at 12:26
  • How long is a (piece of) string? The gain in efficiency will depend on the length of the strings. – Adrian Mole Jun 16 '20 at 12:30
  • 1
    Does this answer your question? [Performance cost of passing by value vs. by reference or by pointer?](https://stackoverflow.com/questions/40185665/performance-cost-of-passing-by-value-vs-by-reference-or-by-pointer) – Jérôme Richard Jun 16 '20 at 12:33

3 Answers3

1

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.

eerorika
  • 232,697
  • 12
  • 197
  • 326
1

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.

Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335
  • Thanks Vlad. I am unfamiliar with the more efficient approach you described which has "string &&firstName". I've not used "&&" before. Could you explain that? –  Jun 16 '20 at 13:54
  • @MikeWeston It is the rvlaue reference that may be bind to a temporary object. – Vlad from Moscow Jun 16 '20 at 14:07
0

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.