Using a function to return a copy of a class, is it usually too expensive?

This is just my intuition.

I'm trying to create a class that will read data from a file, but it feels like it's going pretty bad.

If I have a struct, say

1
2
3
4
5
6
7
8
9
  struct Ancestor 
  {
     string firstName;
     string lastName;

     string BirthYear;
     string BirthDay;
     string BirthMonth;
  };


And I create a class called

1
2
3
4
5
6
  Ancestor readAncestor(ifstream & fin) 
  {
    // Reads the data and creates an ancestor
    
    return Ancestor;
  }



This will mean I'm returning byVal. I will be calling this function inside of a while loop reading it hundreds of times. My question is whether creating a copy could be pretty time consuming? Is returning byVal classes generally a bad thing?

Would it be much better if I did this?

1
2
3
4
5
6
  Ancestor readAncestor(ifstream & fin, Ancestor & currentAncestor) 
  {
    // Reads the data and creates an ancestor
    
    return currentAncestor;
  }


I apologize if this is a technical and dumb question. But it seems like
return byVals of classes can be pretty time consuming and should be avoided.
What do you think?

Thanks!
You generally don't want to do this. Instead, you would pass a reference or pointer to readAncestor that would then hold the new Ancestor. No copy needed.

1
2
3
void readAncestor(ifstream &fin, Ancestor &curAncestor, Ancestor &newAncestor) 
{
}
tl;dr it won't be time consuming, but I'd recommend passing by reference because it is arguably cleaner.

Returning by value is perfectly fine. I imagine somewhere in your code you would have the line:

ancestor = readAncestor(ifs); //might be ancestors[i] if you're using an array or something

Inside readAcenstor, you're going to construct (a.k.a. make) a struct and then when that function returns, you're going to assign the Ancestor you made to ancestor/ancestors[i]. The thing is, construction and assignment take constant time, meaning you won't see a performance difference versus passing by reference. You'd still need to construct the Ancestor you pass to the function before although you'd probably have one less assignment statement, but then again, assignment takes constant time which means it doesn't really play a role. Also, if we're speaking of only a hundred or so iterations in your loop, then any negligible effect wouldn't be noticeable.

Also, your second implementation (passing by reference) means your function doesn't need to return an object. When you modify the currentAncestor object, you are modifying to object passed directly. No need to return it.
Last edited on
> Is returning byVal classes generally a bad thing?

No. It is a good thing.

> But it seems like return byVals of classes can be pretty time consuming and should be avoided.

No. Unless the object is not moveable and/or copying an object has observable side effects.
In general, favour value semantics over reference semantics (for more than one reason.)

Where performance is a concern, one measurement is worth a thousand opinions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <iostream>
#include <string>
#include <random>
#include <vector>
#include <algorithm>
#include <ctime>

std::string random_string( std::size_t min_sz, std::size_t max_sz  )
{
    static std::mt19937 rng( std::time(nullptr) ) ;
    static std::string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
                                  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
                                  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" ;
    static std::uniform_int_distribution<std::size_t> distr( min_sz, max_sz ) ;

    std::shuffle( std::begin(alphabet), std::end(alphabet), rng ) ;
    return alphabet.substr( 0, distr(rng) ) ;
}

static std::vector<std::string> rand_str ;

struct A { std::string a, b, c, d, e ; };

void make_A_ref( A& a )
{
    static std::size_t i = 0 ;
    if( i > ( rand_str.size() - 12 ) ) i = 0 ;
    else i += 5 ;
    // modify object passed by reference
    a.a = rand_str[i] ;
    a.b = rand_str[i+1] ;
    a.c = rand_str[i+2] ;
    a.d = rand_str[i+3] ;
    a.e = rand_str[i+4] ;
}

A make_A_value()
{
    static std::size_t i = 0 ;
    if( i > ( rand_str.size() - 12 ) ) i = 0 ;
    else i += 5 ;
    // return value
    return A{ rand_str[i], rand_str[i+1], rand_str[i+2], rand_str[i+3], rand_str[i+4] } ;
}
void vec_a_ref( std::vector<A>& seq, std::size_t n )
{
    seq.clear() ; // modify object passed by reference
    while( seq.size() != n )
    {
        A a ;
        make_A_ref(a) ; // pass reference
        seq.push_back(a) ;
    }
}

std::vector<A> vec_a_value( std::size_t n )
{
    std::vector<A> seq ;
    while( seq.size() != n ) seq.push_back( make_A_value() ) ; // use returned value
    return seq ; // return value
}

int main()
{
     constexpr std::size_t N_RANDOM_STRINGS = 50'021 ;
     constexpr std::size_t N_OBJECTS = 1'000'000 ;

     rand_str.reserve(N_RANDOM_STRINGS) ;
     while( rand_str.size() != N_RANDOM_STRINGS )
        rand_str.push_back( random_string( 50, 100 ) ) ;

     int r = 0 ;

     {
         const auto start = std::clock() ;
         std::vector<A> seq ;
         vec_a_ref( seq, N_OBJECTS ) ; // pass by reference
         const auto end = std::clock() ;
         std::cout << "use pass by reference everywhere: " << double(end-start) / CLOCKS_PER_SEC << " seconds.\n" ;
         r += seq.back().a.size() ;
     }

     {
         const auto start = std::clock() ;
         std::vector<A> seq = vec_a_value(N_OBJECTS) ; // use returned value
         const auto end = std::clock() ;
         std::cout << "  use value semantics everywhere: " << double(end-start) / CLOCKS_PER_SEC << " seconds.\n" ;
         r += seq.back().a.size() ;
     }
     
     return r != 0 ;
} 

dmesg | grep processor && uname -a && echo ===========
clang++ --version | grep clang && clang++ -std=c++14 -stdlib=libc++ -O3 -Wall -Wextra -pedantic-errors main.cpp && ./a.out
echo =========== && g++ --version | grep GCC && g++ -std=c++14 -O3 -Wall -Wextra -pedantic-errors main.cpp && ./a.out

[ 0.000000] Detected 2200.088 MHz processor.
Linux stacked-crooked 3.2.0-74-virtual #109-Ubuntu SMP Tue Dec 9 17:04:48 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
===========
clang version 3.6.0 (tags/RELEASE_360/final 235480)
use pass by reference everywhere: 2.46 seconds.
use value semantics everywhere: 2.54 seconds.
===========
g++ (GCC) 5.2.0
use pass by reference everywhere: 2.52 seconds.
use value semantics everywhere: 1.42 seconds.

http://coliru.stacked-crooked.com/a/1d913257a1c42468
Last edited on
This will mean I'm returning byVal. I will be calling this function inside of a while loop reading it hundreds of times. My question is whether creating a copy could be pretty time consuming?

No it is not.
As long as there is no second branch where the function might end there is something called return-value-optimization.
That means that the Object you're returning is basically created wherever the output is.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Example program
#include <iostream>
#include <string>

class Test {
public:
    Test() { std::cout << "create" << std::endl; }
    Test(const Test& t) { std::cout << "copy" << std::endl; }
    Test(Test&& t) { std::cout << "move" << std::endl; }
};

Test create_rvo()
{
    Test t;
    return t;
}
Test create_no_rvo()
{
    Test t;
    if(true == true)
        return t;
    else
        return Test();
}

int main()
{
    Test t1 = create_rvo(); // create
    Test t2 = create_no_rvo(); // create and move
    
    return 0;
}
http://cpp.sh/23gw
Last edited on
Wow, I wasn't expecting to get really helpful responses on this. Thanks guys!
@Keene


The thing is, construction and assignment take constant time, meaning you won't see a performance difference versus passing by reference.


Does that mean that, no matter how much data you have in a class, (AKA 5 strings or 10 strings), it would take the same amount of time? Intuitively, I thought 10 strings would take longer.
Last edited on
It means that you should not be stressing yourself out over unobservable inefficiencies. Yes, copying 10 stings technically takes more CPU cycles, but when that time difference is measured in picoseconds can you honestly say that you give a damn?
Last edited on
It's odd, std::clock measures the CPU cycles, right?

Here, when running the program multiple times, the amount of cycles is not constant, what could be the problem?
http://cpp.sh/93xge

Also, an other result for comparing speed of by-value and by-reference ;)
...what could be the problem?

Rounding error, thermal issue, variance in voltage, inductance, derating, communism, gay marriage, it could be anything. Chunks of silicon aren't exactly ideal mechanisms for generating constant frequencies, that's why the SI standard doesn't use it.
... communism, gay marriage
I laughed so hard :D

Rounding error, thermal issue, variance in voltage, inductance, derating,
but yeah, I'd accept it if it was a wall-clock but when measuring the CPU cycles the number should stay constant because you need the same amount of CPU cycles every time, right?
Not necessarily, your CPU can change it's frequency for any number of reasons. In fact depending on the power settings your CPU may cut its frequency in half to save power on a laptop when the charger is unplugged. How much of a variance are you getting? Mine is only a few thousandths of a second.
How much of a variance are you getting? Mine is only a few thousandths of a second.
yeah, something like that but sometimes even a bit more than one hundredth; but well, it's nothing to be concerned about when running it a million times :)
@keanedawg
Does that mean that, no matter how much data you have in a class, (AKA 5 strings or 10 strings), it would take the same amount of time? Intuitively, I thought 10 strings would take longer.


You're right. It would take longer, but that's because they'd be two different classes, and I was speaking more abstractly than anything. If you have a particular class (whatever its data structure may be), it takes c time to construct; a different class may take a different amount of time to construct. The point is that because construction time is constant, the timing of whatever you're trying to do with the class won't be affected by construction.

Now if I may dive a bit deeper down the computer science rabbit hole: This is a generalization, but it's a safe one to make. Some objects do not take constant time. If memory serves me correct, construction of string takes time proportional to the length of the string for example, but regardless, it is so insignificant for most projects that one shouldn't even concern themselves with it. Issues with such low-level performance only arise when you're working with millions or billions of objects, and there is some pressure to do some action as fast as possible.
Topic archived. No new replies allowed.