Parameters and move semantics

Here's some error logging code in my program (basically):

 
void SetError(std::string&& arg){ error = arg; }


1
2
3
4
5
6
7
8
try
{
    // something
}
catch (std::exception& e)
{
    SetError(std::move(e.what()));
}


My SetError() function used to take a reference-to-const. I'm basically just trying to find a use for std::move() after watching a 'back to basics' lecture that used string parameters as an example. I'm thinking this would only be useful if SetError() pushed the argument into a vector?

I sometimes pass in string literals. The exception object isn't used again after passing it in, and it doesn't ever have to be modified. Is there any advantage to using an rvalue reference instead of reference-to-const here, performance wise? Which should I use?
e.what() returns a const char* so std::move doesn't do anything useful here. The std::string object would still have to copy the char data when constructed.
You should use std::move in SetError.

 
void SetError(std::string&& arg){ error = std::move(arg); }

You probably still want to have a const std::string& overload of SetError to allow setting the error with a non-rvalue reference.
Last edited on
Hi,

The std::string object would still have to copy the char data when constructed.

So even if it takes a reference, if the type doesn't match, it needs to copy construct anyway? Does this mean passing a const char* into a function that takes const std::string& would be just as expensive as passing a std::string by value?

You should use std::move in SetError.

I thought that std::move() was just the same as static_cast<std::string&&>() here, and that's what arg aleady was at that point. It sounds like you're saying if the parameter type doesn't match and it has to construct a new object, the '&&' is just ignored, and I should treat it as though it were passed in by value. Is that correct?
Last edited on
So even if it takes a reference, if the type doesn't match, it needs to copy construct anyway?

That's often true, but it depends on what constructors there are.

But in this case it wouldn't matter whatever constructor std::string had because you're passing a pointer. Even if the pointer is an rvalue it doesn't give permission to move (e.g. "modify" or "steal" from) the object that the pointer points to because the rvalue-ness only refers to the pointer itself.

Does this mean passing a const char* into a function that takes const std::string& would be just as expensive as passing a std::string by value?

Basically, yes. The char data would have to be copied in both cases.

But if you already have a std::string object then the const std::string& version would be less expensive because it doesn't have to copy anything.

I thought that std::move() was just the same as static_cast<std::string&&>() here, and that's what arg aleady was at that point.

That's the "declared type" but it's not the type of the expression when you use the variable name.

You might want to use the variable more than once and you might not want to move it the first time.

Example:
1
2
3
4
5
6
7
std::vector<T> vec;
T last_value{};

void add(T&& x) {
	vec.push_back(x);
	last_value = std::move(x);
}

It would be surprising if vec.push_back(x) modified x (it means last_value would not receive the intended value). That's why you need to be explicit when moving from variables (except when using return or throw). It would be too easy to write buggy code otherwise.
Last edited on
Hmm so I'm thinking I should make a third overload for passing literals?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::string error;

void SetError(const char* arg)
{
	error = arg;
}
void SetError(const std::string& arg)
{
	error = arg;
}
void SetError(std::string&& arg)
{
	error = std::move(arg);
}

1
2
3
4
5
std::string str{ "error" };

SetError("error");
SetError(str);
SetError(std::move(str));


SetError(const char* arg)

Pointer to data segment is passed, error dynamically allocates memory for the data if it's too long for short string optimization (one expensive operation).


SetError(const std::string& arg)

Address of str is passed, can't std::move because it's const, error allocates memory for the data if too long for SSO (also one expensive operation).


SetError(std::string&& arg)
arg steals from str, error steals from arg (no expensive operations).


..so actually, the rvalue ref overload would be the same amount of work for passing literals, so no need for the const char* version. Is this correct!? I've been using C++ for way too long to not know how this works.. thanks for bearing with me :)
Last edited on
Have you looked at std::string_view? It was added to C++17 and makes passing C++ or C style strings as a function parameter a lot less expensive.

https://en.cppreference.com/w/cpp/string/basic_string_view
Have you looked at std::string_view?

Oh cool, so I should use this in every single instance that I have a string that doesn't need to be modified? Thanks, I'll look into this too.
A couple of rudimentary std::string_view lessons from Learn C++:

https://www.learncpp.com/cpp-tutorial/introduction-to-stdstring_view/
https://www.learncpp.com/cpp-tutorial/stdstring_view-part-2/

I should use this in every single instance that I have a string that doesn't need to be modified?

Highly recommended, yes.

If you want to modify you could use C++20's std::span. std::span would be useful even if you don't want to modify, it is a lot more type versatile than std::string_view.

https://www.cppstories.com/2023/span-cpp20/#comparing-with-stdstring_view

All the major compilers have C++20 support for std::span:

https://en.cppreference.com/w/cpp/compiler_support/20

I primarily use Visual Studio 2022 Community so all the C++20 features are available, even now the others lack support in a few areas. Modules, for example.

Being a self-taught programming hobbyist I am still getting used to using std::string_view and std::span.

C++17/20/23 can be so arcane at times from what I'm mashed up in the past. ¯\_(ツ)_/¯
LsDefect wrote:
..so actually, the rvalue ref overload would be the same amount of work for passing literals, so no need for the const char* version. Is this correct!?

A move might be a little more expensive but the rvalue ref overload is probably good enough.

Sometimes you even see people pass std::string and other containers by value to avoid having to add multiple overloads.

1
2
3
4
void SetError(std::string arg)
{
	error = std::move(arg);
}

This technique is only suitable when moves are cheap, which they often are, but don't use it on something like std::array.

George PlusPlus wrote:
Have you looked at std::string_view?
LsDefect wrote:
Oh cool, so I should use this in every single instance that I have a string that doesn't need to be modified?

Unfortunately it's not that simple. If you take a string_view as argument but need to pass the string along to another function that takes a const std::string& or const char* then you would be forced to copy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void f(const std::string&);
void g(const char*);

void fun1(const std::string& str) {
	f(str); // no copy
	g(str.c_str()); // no copy
}

void fun2(const char* str) {
	f(str); // copy
	g(str); // no copy
}

void fun3(std::string_view sv) {
	//f(sv); // error, no implicit conversion from std::string_view to std::string.
	//g(sv.c_str()); // error, std::string_view does not have a c_str() function because std::string_view is not guaranteed to be null-terminated.
	
	std::string str(sv); // copy
	f(str);
	g(str.c_str());
}



I don't think you should worry too much about this. Keeping the code simple is also something to strive for. A lot of code that deals with strings are not performance critical so simply using const std::string& is often good enough (that's what we used to do all the time before C++11). For example, if you pass file paths to functions then the writing and reading from those files would probably be the slowest part and copying the file paths once or twice unnecessarily would be an insignificant cost in comparison. If you're passing strings to a print function then the IO operations would be the slow part.
Last edited on
I think I've actually got a handle on this now - thank you very much!
Topic archived. No new replies allowed.