Hello, and welcome to C++ Weekly. I'm your host Jason Turner. I am available for code reviews,
contracting and on-site training. Now a few days ago I put up a Twitter poll asking if my Twitter
followers prefer either a one single return path through a function or multiple return paths, and
I got many responses to this and many comments. I am going to go out on a limb today and I am going
to say that there is one correct answer, and I know this will be a controversial thing to do,
but I will attempt to prove it to you. So the first option - the single return path,
I declare a string, and then based on some boolean parameter, set that string to some value; and then
I return it. Or in the multiple return paths case, I have no temporary retval, I just simply return
the strings "hello," or "world," and you can see that here on the Twitter page. Now what I have
done here is (using the compiler Explorer) set up the first example where we have a ret value that
we then assign based on the boolean parameter that was passed in, and then we return that. I have set
up two different compilers - I have GCC trunk with lib std C++ and I have Clang trunk with lib C++,
so we've got two different standard libraries represented and we've got two different compilers
represented. Now you can just trust me that if I make Clang use lib std C++ instead of lib C++
that we get basically the same thing between Clang and GCC. So I have this code. I have the retval. I
assign it and then (or else I assign a different value) and then I return it. Now we need to walk
through exactly what is happening here. We have in this code specifically requested the compiler to
default construct a std string, and it has obliged us for that; and we can see these things like:
this '_M_create' here and we can (we don't see that necessarily in the lib C++ version of it)
and then we have our assignments. So we have asked for a default constructed thing, and then
we've explicitly asked the compiler to perform an assignment operation here, and we can see (in
the lib C++ version) we can see calls to these functions for assign that takes a const char*.
Now it is very much possible that the compiler, if it had full visibility into everything here,
would be able to say, "Oh, I see what you're doing here -you have default constructed this thing,
you didn't use that initial value. Now you are doing an assignment operation to it,
so I know that really the internal representation is only being used once and now we can return
that," and it would be able to optimize the whole thing away. However, we don't have any compiler
that can do that, and by asking the compiler to do more we are making it less and less likely that it
can do something optimal. Now if we compare this instead to the version that uses a ternary, which
I had a several people comment that they would have preferred, I can say "if B then return hello,
else return world," and I will go so far as to say that this is also the wrong answer, and I
will justify that. We have our two strings 'hello' and 'world' and we have our ternary here. Now,
it is very important to understand exactly what we are asking the compiler for here as well. We are
not returning simply a string from this function. We are returning specifically a const char*. We
are returning the result of this expression, which then must be used to instantiate the
string object in the return value, and we can see that the compiler is pretty smart about this. If
we look at the GCC version specifically, it does a test of this input parameter and then depending
on that test it's going to copy over whichever pointer for whichever string it's going to use
and conditionally move that data in to the return value for the string, and then it sets the length,
which is five. It's five because both of these const char*s happen to be five. Now things don't
go very well if we make one of them six. It must now do considerably more work to decide which one
it's actually copying in, and now we can actually see calls to 'strlen' because it doesn't know if
this is a five character string or a six character string. We've gone one step past what our compiler
is able to optimize here. Now we will take it to what I say is the correct answer. If
we did... (and I'm going to leave this here so we can come back to it). In this case we have
given the compiler all of the information that it could possibly hope for. It knows that this
is a five character string that is being used to initialize this return value. Return value
optimization's going to come into play. Everything works like we expect, and we can see it here where
it tests this input parameter and depending on which (whether) the value is true or false,
it jumps down here and we don't even see a string literal in our binary at all anymore. We now see
our string literal encoded as a single 64-bit value, so the 64-bit value is copied into place
in this 'rdi+16', so basically it is able to copy the value directly into the return value;
and we can actually literally see here in the disassembly the return value optimization
working for us; and we can see the lengths of the string: 5 and 5 here. Now if we make one of these
a mismatched size string, then we see that we're just going to get 6 here; and if we get this past
something that can fit in a 64-bit value, which is eight characters, so we'll just do that 12345678,
so this should be a single 64-bit value. Now if we add it to 9, we still have our small string
optimization coming into play, and the compiler is really doing some interesting things for us
of setting up the first part of it and then copying in the remaining part of the string.
Taking advantage of the small string optimization, and with both standard libraries, both compilers,
it can see through this and it does something similar with how it is encoded this data and
how it is copying it in piecemeal as it can. Now let's go back to our ternary version. Now part of
our problem with the ternary is that the compiler doesn't have this visibility into which string
length is actually going to be used at runtime. Now we can actually fix this if we actually create
a ternary that does what we want it to do - now this ternary has a return type of type 'string',
it is no longer a const char*. We're going to see the exact same optimizations come into play again.
So that's good if we use our ternary correctly. If we go back to this version, now I got several
people that stated that they wouldn't have an else block here, personally I really don't like
code that doesn't have the else block, I kind of want to see the if-else, particularly in a small
function, but that's fine; leaving out the else block does the same thing. And we got one final
possibility that people suggested, and that is that this should return a const char*. Now this
possibility is interesting because it is simply returning a pointer to a const character literal.
This is perfectly legal and our code basically goes away to nothing, but we have taken this
extra step of obfuscating further the length of our string. It's the same problem that we have
with the ternary. So if your ultimate requirement was that this value be put into a standard string,
then you're not going to gain anything by using this 'get_value' returning one or the other, but
it is definitely something worth considering, so let's play with that for just a moment. So making
this interestingly our get_string with Clang and lib C++ goes away to nothing. It does fully
see through this code and know that we're not actually using the return value from get_value,
but let's go ahead and make it do something more interesting where we're actually using this to
generate a string. So we can see the code here. It has to generate these calls to 'memcpy' and it has
to generate probably a call to actually calculate the length of the string somewhere - there we go,
called a 'strlen' here because it's not able to see all the way through this again, it's just
beyond what the compilers are able to optimize. So if we know that it needed to end up in a string,
then we should ultimately have it in the string, and if I change the get_value to
be put in a string here, then our get_string is going to very simply inline the whole thing and
it all goes away again. Now the third option, or the last remaining option at this point,
the nth option wherever we're at, is that this should have returned a string view possibly.
We are compiling with C++ 17 and fortunately, I think, there is no implicit conversion from string
view to string, but we can initialize with this a string from the string view that was passed in;
and we still unfortunately aren't able to get the kind of optimization we wanted. So definitely if
you wanted a string the entire time, you should have put it in a string, but the thing that we
can see is that there is now with GCC no longer a call to 'strlen' so that is at least something.
With the string view, it doesn't need to calculate the length of the string because it knows that is
the distance between the first pointer and the second pointer, the begin and the end of the
string view. So there you have it. This is why I say there is exactly one right answer for how you
should return from a function, and it all comes down to understanding the lifetime of our objects
and understanding what this means. So we could have explicitly created our string objects here,
but in some permutation of these things with multiple return paths, with either a standard
string or not a standard string, and having the explicit else or not having the explicit else,
these are the only places where we can get the optimal code generated from the compiler short
of using a ternary that has explicit strings in it. This episode definitely went longer than I
expected it to, but I really want people thinking about what you're asking the compiler to do, what
the lifetime of objects means in our C++ code, and really how to work with the compiler; and besides
the fact that this is all fewer lines of code, so that makes it less code to maintain as well
in the future. If you say, "Well this function is trivially short. This is pointless," I disagree. I
say that your functions that are generating values should be trivially short. We want to be taking
advantage of return value optimization, copy elision, and move elision as much as possible.
If you saw Tony Van Eerd's talk from C++ Now 2017 on post modern C++ where Tony asked,
"How many times must you see a block of code before you put it in a function?" and ultimately
his answer was once. I agree with this. This is the way that we can write the most efficient code
that's the most maintainable and optimal. So I'm sure I will have many people disagreeing with me.
Feel free to do that, but definitely make sure you subscribe and check out the other videos I have.