Are the Days of Passing Const Std::String & as a Parameter Over

Are the days of passing const std::string & as a parameter over?

The reason Herb said what he said is because of cases like this.

Let's say I have function A which calls function B, which calls function C. And A passes a string through B and into C. A does not know or care about C; all A knows about is B. That is, C is an implementation detail of B.

Let's say that A is defined as follows:

void A()
{
B("value");
}

If B and C take the string by const&, then it looks something like this:

void B(const std::string &str)
{
C(str);
}

void C(const std::string &str)
{
//Do something with `str`. Does not store it.
}

All well and good. You're just passing pointers around, no copying, no moving, everyone's happy. C takes a const& because it doesn't store the string. It simply uses it.

Now, I want to make one simple change: C needs to store the string somewhere.

void C(const std::string &str)
{
//Do something with `str`.
m_str = str;
}

Hello, copy constructor and potential memory allocation (ignore the Short String Optimization (SSO)). C++11's move semantics are supposed to make it possible to remove needless copy-constructing, right? And A passes a temporary; there's no reason why C should have to copy the data. It should just abscond with what was given to it.

Except it can't. Because it takes a const&.

If I change C to take its parameter by value, that just causes B to do the copy into that parameter; I gain nothing.

So if I had just passed str by value through all of the functions, relying on std::move to shuffle the data around, we wouldn't have this problem. If someone wants to hold on to it, they can. If they don't, oh well.

Is it more expensive? Yes; moving into a value is more expensive than using references. Is it less expensive than the copy? Not for small strings with SSO. Is it worth doing?

It depends on your use case. How much do you hate memory allocations?

std::string or const std::string& argument? (the argument is internally copied and modified)

Ran some tests, with G++ 4.8.1 on Linux Mint x64.
Flags: -std=c++11 -O3 -DNDEBUG

void doSomethingWithString(string& mString) { mString[0] = 'f'; }

string getCopy1(const string& mString)
{
string result{mString}; doSomethingWithString(result); return result;
}

string getCopy2(string mString)
{
doSomethingWithString(mString); return mString;
}

int main()
{
string s{"132958fdgefi9obm3890g54"};
string t{""};

{
startBenchmark();
for(int i{0}; i < 20000000; ++i) t = getCopy1(s);
log(endBenchmark(), "getCopy1 variable");
}
{
startBenchmark();
for(int i{0}; i < 20000000; ++i) t = getCopy1("abcsd");
log(endBenchmark(), "getCopy1 literal");
}

{
startBenchmark();
for(int i{0}; i < 20000000; ++i) t = getCopy2(s);
log(endBenchmark(), "getCopy2 variable");
}
{
startBenchmark();
for(int i{0}; i < 20000000; ++i) t = getCopy2("abcsd");
log(endBenchmark(), "getCopy2 literal");
}

return 0;
}

Output:

[getCopy1 variable] 1236 ms
[getCopy1 literal] 1845 ms
[getCopy2 variable] 993 ms
[getCopy2 literal] 857 ms

Conclusion:

getCopy2 is faster, especially with rvalues (literal strings).

What happens when a string literal is passed to a function accepting a const std::string & in C++?

When you pass a string literal to a function that accepts const std::string&, the following events occur:

  • The string literal is converted to const char*
  • A temporary std::string object is created. Its internal buffer is allocated, and initialized by copying the data from the const char* until the terminating null is seen. The parameter refers to this temporary object.
  • The function body runs.
  • Assuming the function returns normally, the temporary object is destroyed at some unspecified point between when the function returns and the end of the calling expression.

If the c_str() pointer is saved from the parameter, it becomes a dangling pointer after the temporary object is destroyed since it points into the temporary object's internal buffer.

A similar problem will occur if the function accepts std::string. The std::string object will be created when the function is called and destroyed when the function returns or soon afterward, so any saved c_str() pointer will become dangling.

If the function accepts const std::string& and the argument has type std::string, however, no new object is created when the function is called. The reference refers to the existing object. The c_str() pointer will remain valid until the original std::string object is destroyed.

Replace const std::string passed by reference, with std::string_view

No it's not equivalent.

There are two cases where using std::string const& is a better alternative.

  1. You're calling a C function that expects null terminated strings. std::string_view has a data() function, but it might not be null terminated. In that case, receiving a std::string const& is a good idea.

  2. You need to save the string somewhere or you're calling a C++ function that expects a std::string const&. Sometimes they are function from libraries that would be undesirable to change.

All other cases would be better with std::string_view

There is also some key differences between a string view and a reference to a string.

First, you are passing by reference, not by value. The compiler has to reference it everytime it want to access the capacity, size or data.

Second, a string view don't use capacity, only a size and a data. It also means that loads can be omitted since you are passing it by value, as a local variable with limited scope.

Pass arguments as std::string or const std::string&?

The canonical answer in your situation would be to pass the argument by const&, both for reasons of performance (it is guaranteed to avoid copying the string) and because you document intent. The former is only important if you have profiled your code and determined that passing the string is a bottleneck - if it isn't, you're mostly looking at "best practises" rather than making a big performance difference.

However to me, if I look at the signature of your function, the second one clearly states "I will only read your parameter and not do anything to it", whereas the first one pretty much states that you will be doing something to the parameter, even though your changes will not be visible to the outside as you are working on a copy of the parameter.

There is also the added advantage that passing the argument by const reference avoids memory allocations, which is handy if you are working on a system that doesn't have infinite memory (ie, all of them).

c++ const ref vs template concept

As you presented it: types which are convertible to std::string wouldn't need to be converted before the call to std::cout (potentially saving memory [de]allocations).

It's the only big advantage I can see looking at your code. However, flipping a bit or two:

template <class T>
concept String = std::is_convertible<T, std::string>::value;

void writeMsg2(String auto const& msg) {
std::cout << "msg = " << msg << "\n";
}

Live on Compiler Explorer

Why does passing a const std::string& to a constructor not allow you to bind it to a const std::string& member variable in the initialiser list?

but then I can't call the constructor with a string literal.

If you initialize the object from a string literal, a temporary std::string will be used to pass to the constructor of Module. The temporary could be bound to lvalue-reference-to-const, but it will be destroyed after the whole expression immediately, after that the member reference m_name becomes dangled. Getting it from Module::GetName() and dereference on it leads to UB.

You can use a named variable instead to initialize the object; and you still need to notice the lifetime of the variable. Or stop using the member reference.

Why can an array of char be a template parameter but a const char* can't

The error message from the compiler is clear enough:

error: 'Test::teststr' is not a valid template argument because 'Test::teststr' is a variable, not the address of a variable

So you need:

#include <iostream>

struct Test {
static const char* teststr;

template<const char **str>
void p() {std::cout << *str;}
};

const char* Test::teststr = "Hello world!";

int main() {
Test t;
t.p <&Test::teststr>();
}

And then it works - the point being that [the contents of] a variable is not a compile-time constant, whereas the address of a variable (if it's a static or global variable) is.

C++ Pass a string as a value or const string&

Yes, you should use std::move, but not like this. The proposed piece of code would try to hash moved-from strings (pre-C++17 it was unspecified if the strings would be already moved from at that point, see rule #20 here).

You should pre-calculate the hash and store it in a variable:

auto h = hash(name, value);
g_data[h] = {std::move(name), std::move(value)};

You should NOT pass string name, string value by const reference, since it would prevent you from moving them.



Related Topics



Leave a reply



Submit