Efficiently creating a class inside a class method and passing it out of there?

Viewed 73
    CList<CString> Split(char delim) {
        CList<CString> subStrings;
        for (char* ps = Buffer(); *ps; ps++) {
            char* pe;
            for (pe = ps; *pe && (*pe != delim); pe++)
                ;
            subStrings.Append (CString(ps, int (pe - ps)));
            if (!*pe)
                break;
            ps = pe;
        }
        return subStrings;
    }

    CList<CString> values = s.Split(';');

The final statement causes the list's copy constructor to be called two times: First time when subString gets passed out of the method. The instance on the stack local to Split() gets copied (to only the compiler knows where) to persist long enough to be copied again to "values".

How would I go about not having two (rather expensive) list copy operations here without fundamentally changing my code (like e.g. adding a CList& parameter to Split())?

Edit:

Compiler flags are set to use c++20.

Edit 2:

Small test project.

https://drive.google.com/file/d/1f1GVRJ9lbu2nYnhsLLv3YbCjtKr8m6-k/view?usp=sharing

1 Answers

I ran some tests with this code (using visual studio 2019), and I observe differences in behavior in debug and release builds. The debug builds DO call copy constructors and don't do RVO/copy elision. Release builds do the optimization and no unecessary copying is done. This example shows you how to split strings with minimal copying (in release builds)

This result did surprise me somewhat, so thanks for asking this question. I did learn something to today :)

The output of the release build is as expected (emplace_back doesn't copy either):

-----------------------------------------------------
calling split function

my_list::my_list
  my_string::my_string
  my_string::my_string
  my_string::my_string
  my_string::my_string
-----------------------------------------------------
result of split

the
quick
brown
fox

-----------------------------------------------------
cleanup vector starting
my_list::~my_list
  my_string::~my_string
  my_string::~my_string
  my_string::~my_string
  my_string::~my_string

However the debug build clearly doesn't do that kind of optimization :

my_list::my_list
  my_string::my_string
  my_string::my_string
  my_string::my_string
  my_string::my_string
  my_string::my_string(const my_string&), copy constructor
  my_string::my_string(const my_string&), copy constructor
  my_string::my_string(const my_string&), copy constructor
  my_string::my_string(const my_string&), copy constructor
my_list::my_list(const my_list&), copy constructor
my_list::~my_list
  my_string::~my_string
  my_string::~my_string
  my_string::~my_string
  my_string::~my_string
-----------------------------------------------------
result of split

the
quick
brown
fox

-----------------------------------------------------
cleanup vector starting
my_list::~my_list
  my_string::~my_string
  my_string::~my_string
  my_string::~my_string
  my_string::~my_string

This is the test code :

#include <iostream>
#include <vector>

struct my_string
{
    my_string(const char* from, const char* to) :
        value(from, to)
    {
        std::cout << "  my_string::my_string\n";
    }

    my_string(const my_string& rhs) :
        value{ rhs.value }
    {
        std::cout << "  my_string::my_string(const my_string&), copy constructor\n";
    }

    my_string(my_string&& rhs) :
        value{ std::move(rhs.value) }
    {
        std::cout << "  my_string::my_string(my_string&&), move constructor\n";
    }


    ~my_string()
    {
        std::cout << "  my_string::~my_string\n";
    }
 
    std::string value;
};


struct my_list
{
    my_list()
    {
        // note reserving some more room up front will reduce reallocations 
        // try commenting this out and you will see many more strings created/destroyed
        // because of vector reallocation
        strings.reserve(128);
        std::cout << "my_list::my_list\n";
    }

    my_list(const my_list& rhs) :
        strings{ rhs.strings }
    {
        std::cout << "my_list::my_list(const my_list&), copy constructor\n";
    }

    ~my_list()
    {
        std::cout << "my_list::~my_list\n";
    }

    std::vector<my_string> strings;
};


my_list split(const char* string, char delim)
{
    my_list list;

    for (const char* ps = string; *ps != 0; ++ps)
    {
        const char* pe{ ps };
        while ((*pe != 0) && (*pe != delim)) 
        {
            ++pe;
        }
    
        list.strings.emplace_back(ps,pe);
        ps = pe;
    }

    return list;
}

int main()
{
    std::cout << "-----------------------------------------------------\n";
    std::cout << "calling spit function\n\n";

    {
        auto mylist = split("the,quick,brown,fox", ',');

        std::cout << "-----------------------------------------------------\n";
        std::cout << "result of split \n\n";

        for (const auto& mystring : mylist.strings)
        {
            std::cout << mystring.value << "\n";
        }

        std::cout << "\n-----------------------------------------------------\n";
        std::cout << "cleanup vector starting";
    }
}
Related