We know that the compiler can optimize many things. But we also know that the compiler does not know our intents at the highest levels of abstraction. This episode made me realize that we should know exactly what is the highest level the compiler can understand our intent. Because it can not optimize further than that. Also now I understand the value of what Mr. Stroustrup always says: The code should reflect our intent. On reason for that is that the compiler also understand our intent and optimize things away.
iirc, the whole "one return" policy started with the necessity to guarantee that, in C, calls to release handles to system objects are ran every time. So, yeah, in full modern Raii codebase, it no longer make sense, and you can freely avoid creation of an empty object.
Yup. It is also a MISRA C rule, and a rule I hated absolutely the most. Not only it made the code much harder to write and read, but it also completely wrecked the cyclomatic complexity (that was also being checked...). And the best part? Drivers we wrote didn't allocate / release any resources at runtime.
That's a practical benefit, but I think the idea also comes from a misunderstanding of something in the original "structured programming" paradigm shift some time back. It refers to "single return" but it has nothing to do with how many return statements are inside a function; rather, it has to do with where the code resumes after the function returns: it's such an alien concept today, but you could have different success/failure return points or more generally fold conditional branching on the return value into the function code itself. The closest thing today would be a function that throws a different exception to classify the result, rather than returning a value representing that classification, and you invoke it using a try block with different catch's for each case. Finally, another practical benefit for having a single return statement is ensure that RVO is triggered, by turning it into NRVO instead which always works.
Very interesting ! But why letting this « else » statement in multiple return path ? Readability and maintainability ? I do prefer multiple return path because it allows to remove lots of extra lines/indentations level. In the end, the less lines you have to deal with, the easier it is to review the code.
I suppose that at the time a pointer to a char seemed like a neat and simple way to manipulate text that didn't require much overhead, so a native string type seemed redundant. I'm only guessing though.
While working on the bytecode-VM for my little programming language I noticed a big performance boost using mutiple return paths. Even in void functions. if and return was much faster than a chain of if else. About 20-30% speed gain. I got those results on GCC-10 with -O3 and c++20. The compiler was able optimize a lot more in this case.
I thought multiple return paths were the correct answer, but for a different reason. Doing multiple returns allows the compiler to verify that the condition is exhaustive. Now, as long as there's an "else", that's not much of an issue, but you might be tempted to have some more complicated logic in place which you think is exhaustive but it isn't. In that case the compiler would force you to reconsider and at the very least throw or assert or whatnot. In the single return path version, you might just return the default constructed (or uninitialized for PODs) value.
I recently returned to programming in C++ after many years of not programming, and as I was developing my application I had to ponder many returns verses a single return when I had a situation that required the calculation based on many parameters. I didn't consider optimum code generation, rather for me it was more important that the code be easy to read. With a single return path, either every evaluation criteria has to be nested, (which leads to horrible levels of indentation (impossible to read and work out what is or isn't true)) or, every evaluation has to "exclude" all the conditions that have been checked but aren't true, also horrible to workout what is or isn't true. By using multiple return paths, one condition at a time is evaluated, and if true, we know we're going to leave and never have to worry about it again in the rest of the function. As you say, if you get to the final return, either something is wrong with the conditions to evaluate against and we can either return the default constructed object or uninitialised POD or through an exception or assert(false) or we are okay and return a harmless value.
Some of us are stuck with targeting code for RH/CenOS 7 which does not have SSO but uses the "old" string implementation in the standard library. That prevents the single-64-bit constant code from working, as it always has to allocate and copy. I'm currently leaning to using sv suffix instead of lexical string literals, to preserve the length knowledge _in general_ when the compiler deals with const char* as strings.
This seems like the wrong conclusion. It's not that you should use independent return statements but that you should not use C-types as strings. If you use strings, why are you using single-value return statements to the implicit constructor? Isn't the conclusion instead that you should use single-value return statements if you want to rely on implicit constructors? I like the episode, it makes a lot of sense to keep in mind that compilers are not always able to do their magic for allocating types.
My thoughts exactly. It's not about `return` at all. You get the same issue when writing: const std::string prefix = someflag ? "value one" : "the other one"; and because of the desire to use const we are led to using the ternary operator. But this will generate bad code for the same reason. The issues in the code are actually: - using a computed value as the argument of a constructor, _especially_ in the case of lexical string literals where the optimizer can constexpr the strlen when the value is known at compile time; - assigning to a default-constructed instance rather than initializing it when you are ready for it. - ensuring NRVO vs _maybe_ getting NVO if every return statement is compatible with that. I think the real benefit to presentations like this is that it encourages people to try their own ideas and play around in Compiler Explorer.
> 12:51 _"one [line of code to a function]"_ won't that increase the function calls and calling records on stack? I genuinely wanna know ur thoughts! The book chtp-6e (quite old) or c++htp-8e by deitel-deitel always emphasize on reducing function calls for small things.
(0:22 in) I prefer multiple return paths only IF the conditions are simple and it allows me to avoid nesting a whack of if statements or more than a depth 1 loop break (or using status variables to force multiple loop breaks). if (A) { ...} else { if (B) {...} else {...} } is much less nice than if (A) {... return} if (B) {....return} C. I love your videos a lot (and have only recently got back into C++ after a 10 year hiatus, so I'm trying to catch up on C++ 11 / 14 / 117) and you have taught me a lot of fascinating things, but in some cases, I think that clear, easily read code with less nested blocks, while possibly being less efficient, is preferable to the hyper-efficient but less readable code. Many C++ programmers are obsessed with optimized code, which often isn't necessary, and this is coming from someone who has a PhD in combinatorial design theory where even small optimizations can make the difference between tun times in hours vs. days.
Not as well as you'd expect, because you have to consider the result type of the ternary expression. It's a `const char *` as opposed to a `const char []` (which is what "Hello" is) so the compiler is not able to optimize in the same way as it can with two completely different return statements.
I couldn't really follow the difference between using the ternary operator and the if else clause, other than some internal compiler logics. From my point I "see" an implicit else clause in the ternary operator, it should in my mind be the same. I see comments below mentioning the difference that the ternary operator is more of an atomic "macro" like operation rather than an "evaluation" option, in that case I would consider the ternary operator to be questioned in all possible optimisation cases.
The ternary operator with string literals is treated as an expression with temporary type `const char*`, which does not include length. That const char* is then used to construct a std::string, so in the general case where the two result operands could be a const char* created at runtime from a function call, it would not be optimised. I don't know why it isn't optimised in this video tho
I agree with this completely, and I'm gratified to come across a like-minded soul. The "single entry, single exit" rule is an ancient, outmoded practice that hails from the Fortran days. If early returns are a problem for you, then I would submit (as Jason more-or-less does) that your coding style has other, worse problems. I would further contend that if minimizing time to market and programmer pain is a higher priority for you than maximizing performance, then you shouldn't be working in C++ at all. All that said, it's not my impression that this post was intended to make any recommendation concerning how to approach writing production code, but rather merely to point out which of these idioms is more optimal as far as performance goes. Very interesting and informative post.
What do you think of the "my string"s (string literals) method for creating an std::string? I would have used something like: std::string getValue(const bool b) { using namespace std::literals; return b ? "Hello"s : "World"s; }
It would be interesting to explore this concept with other return types….not sure the general recommendation “prefer multiple return paths” is optimal in other cases (including switch statements). For return types that fit into registers I would guess perf would be comparable for either multiple/single path. Also this simple example is a bit contrived since real world scenarios typically have a chunk of “other stuff” going on in the additional paths.
For trivial objects of any type or size this example becomes irrelevant. The reason this example is not contrived is because we rarely think about trying to make as much of our API trivial as possible. Also, yes, you can *almost always* refactor your code to look like this by composing multiple functions, which can result in much more optimizable code. See also: ruclips.net/video/ZxWjii99yao/видео.html
A ternary isn't a branch in execution, but rather a value. An if/else leads to 2 different paths where different instructions are executed. A ternary represents one of the 2 values on its right side. So an if/else is different stuff to do. The ternary IS the string or integer or whatever you put in there.
Ternary evaluates to a const char *, which then must be used to initialize a string, but since the const char * came from a ternary it's length is not known hence there's extra work to do.
@@hex7329 The ternary makes the const char* a calculated thing, so the compiler no longer knows the length without calling strlen. The if/else constructs a different string on each branch. It's not that if/else is different per-se, but you are also doing different things in the branches in each case. The ternary would be equivilent if you put a std::string in each branch of it rather than factoring it out and constructing the string around the result.
heaving return type as const std::string_view would be better replacement for cont char*, since you only need return "literal"; without std::string("literal") Ofcourse if you want to mutate the returned string and you do not really care about heap allocation, then return should be std::string.
@@tatoute1 right, but I am only saying that if you necessarily have to return const char*, rather use std::string_view since it can be used safely e.g. over range loops
I tend to mix a little bit. If I don't have all the info I need till later I might create a ret variable to have something to write data too as I'm reading it. But when I have a branching path where it's like. Is the data compressed or not? Then i'll use multiple return paths. I'm not sure how correct this all is but that's how things are. Thank you for all these videos.
Great episode. It is easy to not think about what the compiler will do at all. Tho no every programmer will have an encyclopedic (and updated) knowledge of every compiler to know these things from hearth when in doubt you can at least check the generated assembly. ^_^ if (b) return "foo"; return "bar"; //ftw
There is also an opinion, that having multiple return statements can cause RAII bloat, with each statement generating a different destruction code (because of the scope it is being in).
That is interesting. If so does the destructor change the instruction binary according to the context it is being called with (e.g. data members change inside scope)? That would then be a compile time optimization and lead to mlre performant code (with the caveat that it bloats the binary)
Try returning a const char* instead of a std::string which is way overengineered for the problem. Just because you can use STL instead of C does not mean you always should.
No you absolutely always should. In fact you shouldn't even use C++, use Rust. But if you have to use C++ for the love of god avoid raw pointers like the plague
@@TheMrKeksLp Rust is dead. C++ 20 is King, and raw pointers are fine for me. I don't have problems with them, and I certainly don't need to wrap up every piddling constant string in a class.
I'm disappointed that the compiler can't figure this out. I know it's easy to have a strong opinion on how compiler writers do their job. At the same time however it is difficult to defend or advocate the virtues of c++ because of stuff like this. We have to do better. The language is complex enough without having to concern one self with gaming the compiler.
I have some strong opinion around this topic since I have an instructor in my university forbid us using multiple returns. The reason is basically "this is a best practice that my instructor forced me to do." If her argument was something more reasonable like resource management concern, then I will at least try to debate.
About 35 years ago I was in my first year of a Chemical Engineering degree and we had 1 lecture a week on programming. Because the University has a computing science faculty, we were taught the CompSci approved language for programming, which was PASCAL. At the end of the academic year we had a short cause on FORTRAN because that was the computing language that industry actually uses. And a few years later, I did begin using FORTRAN for my job, and have never heard of anyone in industry using PASCAL, (although IEC 61131-3 Structured Text for programming programmable logic controllers is quite close to it). Tip, if you every want to upset an instructor, just prefix a comment on their course with "In the real world".
Of *course* it shouldn't be `const char*`. You may or may not need to put it into a `string` right away, but if you don't want to do that you should at least use a `string_view`.
Count me as someone that prefers an implicit else. ;^} I’m generally more concerned about understanding the context of the multiple conditions and structuring the returns such that the most probable condition returns first. It’s true that sometimes you won’t know... At which point it’s necessary to break out the tape measure (if you can ever find it).
Same. I find it weird that the folks at clang think this is a problem for readability - I find it to be the opposite. clang.llvm.org/extra/clang-tidy/checks/readability-else-after-return.html
@@superscatboy he said implicit, as in, implied but not explicitly there. So I think he means he prefers clangs style, and I do as well for the record. It reduces visual noise, particularly in the case of return values and so on. 😉
That else has no reason to exist in the multi return except to have extra indentation. This example it does not really matter though. I like the multi returns. I remember professors pushing for the one return method.
the else doesn't matter ... except when it does. Example: if constexpr, if consteval, and older gcc compilers in the case of a constexpr function that just happens to have an if statement in it. I always use the else, it makes me happy and its consistent with my code.
The always correct answer is: "follow your company's style guide." But if you have a choice, do what makes for the most readable code. If your compiler makes a sub-optimal job of it and you actually care, change your compiler.
"follow your company's style guide", no, fight stupidity in style guide. If the style guide choose between CameCase or use_underscore, fine. If the style guide is about the number of return or the length of the function, contest it. Mandatory things like this is bullshit stupidity, created by morons, just to find something to says in code reviews. Here is the ultimate code style: - make your code understandable - use whatever tools the compiler offer you to reduce the probability of bugs (ex make everything const except if not possible). - do not use new forms/things/syntax just because it exists. If you want to experiment, do it outside of production code.
awesome video..& videos :) I have a question ; is it safe to return a string_view from a function looking at the example in the video. Does not the value that string_view will be destroyed ? Just asking to learn...
Do not return string_view. String view borrow the string content somwhere and offer no garanties about this somewhere lifetime. It's open bar for dandling pointer.
It is safe for string literals like in the example. std::string_view f() { // Safe return "hi"; // p points to a string literal auto *p = "fine"; return p; // Unsafe, returns pointer to local storage return std::string{"bye"}; std::string s{"bad"}; return s; }
So just don't ask the compiler to default-construct your retval; std::string get_value(bool b) { //consider b==false most of the time std::string retval{"Hello"}; if(b) //sometimes its true retval = std::string{"Worlds"}; return retval; }
I assume your declaration was something like `constexpr char* blahblah();`? If so the return type is `char*`, not `const char*`. You're marking the function as `constexpr`, not the return value. In that context, it wouldn't make sense either. And indeed, conversion from a string literal `"hello"` to a non-`const` `char*` is illegal in C++, hence the warning.
Understanding where and when return value optimization comes into play, when it is required, and structuring your code such that you take advantage of it, matters for anything that is bigger than about 2 registers.
Hallo, what is your opinion about using continue and break in loops? I tried Compiler Explorer with gcc 8.2 and -O3 flag and following code: #include void calc(){ for(unsigned int i = 0; i < 1000; i++){ if(i%2!=0) continue; std::cout
Interesting insights, but I also think you are putting too much emphasis on optimization. In general, one should prefer code that expresses the intent most clearly and succinctly, which in this simple case is by the ternary. Compilers are getting smarter all the time, and unless it is really necessary, one should not write code that is harder to read to accommodate a compiler that will be dumb by tomorrow's standards. As for the return style, I think the disconnect is that people that argue for single-return are thinking about functions that are much too long and complex to begin with. I prefer multiple returns, structured to first get any preconditions out of the way one by one, with the actual work at the bottom of the function. But these functions are hardly ever longer than half a screen (at 80 columns), so I'm never confused as to where all my returns sit (also helped by syntax highlighting and a blank line after each return).
People have been saying for decades that smarter compilers will take care of it. While they are getting smarter, they're just never quite there. Looking at generated code is still often a nightmare. It's okay if you aren't doing that much work. But if you want or need your code to be lean, it can be helpful to be aware of patterns a compiler can or can't turn into something good. I'd recommend not overfitting your style to a particular compiler, but choosing constructs that translate to good machine code with few transformations. That should work on a range of compilers and not be too hard on you. I agree multiple returns can be clean and readable. Speaking more generally, after carefully splitting code paths based on preconditions, don't make them join again unless there's commonality on what needs to happen next. Obsessive joining only makes it hard to tell when a particular condition is handled and done. Paths applying to many conditions are more complex and bug-prone. Anything could potentially still happen, either by design or by bug. You won't know without analyzing the entire function.
I guess there is a performance lesson to take away from this video, but it's not about return style at its core. The real key seems to be that object construction from literals should be done explicitly and straight from the literal, because a compiler is most likely to take shortcuts with a source literal that is immediately visible as a constructor argument. If the value used for initialization is obscured even by just very simple logic (such as a ternary) before going into the constructor, these optimizations might already fail to kick in. Returning literals from a function by multiple return is how you get literals into the implied return type constructor directly, but that is just one (albeit clever) application of a larger principle. He kind of took the wrong path, but still got to the right conclusion :-)
-11:50-- i think all of people would prefer longer videos over 5-6 min video.- I'm wrong. however i would still like it if you could do a longer video once a in a while.
That's a shame. I certainly prefer longer videos, but if thats what the stats say then you have no real incentive for the extra effort. If you feel like experimenting, you could try longer ones for Patreon supporters, while using short ones for open youtube as patreon bait (A 5 on the scale from 0 to Bryan Lunduke). Also I'd like to see some more shout outs for CppCast. I've been following you for quite some time but only came across the podcast last week. It's a great listen!
I knew I had seen the name somewhere! Eh well, fool on me for not checking it out earlier. Though I keep a close eye on the std proposals forum, I'm somewhat removed from the progress further down the pipeline, and the podcast is a great way of keeping up with proposals, papers, notable opinions and viewpoints from developers who work in niche areas.
Could you do a video about inline and differences between inline in method declaration vs inline in method definition? I've always thought inline in definition inlines methods body to the class' body while inline in declaration suggests the compiler to replace function calls with function's body. Also I could never come up with an example to show the advantages of `inline` in Compiler Explorer
General video concerning inlining and compiler optimisations would be nice. I got an impression that lately inline doesn't do much. Just little tip for a compiler. In my first compilers it meaned that it will inline that function.
reconn it used to have a *lot* to do with whether or not a compiler would inline something. A little like the now deprecated (or maybe actually removed?) register keyword that hinted to the compiler that it ought to keep a variable in a register.
@@mikezhu5894 : Yes maybe so. But that is a quality of implementation of the compiler. There is a std::string constructor which takes a char *, a size and an allocator. The compiler knows the string length of either option. So it could easily optimise a call to a string constructor of 3 parameters, losing the 1st parameter in some form of conditional move. And after this do further optimisation of the constructor so it is still a string but it is constructed inline. So yes, compilers can go further with this construction as Jason noted in his clip.
This function is trivially short and does not reveal how to work with the compiler to make it optimise the return value. You are not returning literal strings in real code.
Why not something like this? It shall be as fast as returning a const char *, if im not wrong. const std::string & get_value(const bool b) { static const std::array strings = { "1234567890", "qwerty" }; return (strings[b]); } Edit: my remark is more about the return type and the static string than the array trick (it was faster to type than the if/else variant)
I've recently started doing static constexpr char* to define all my return arrays. Then, my if else, or switch case, and return the char* by name. Is that equivalent to your suggestion? E.g. static constexpr const char* a="A"; static constexpr const char* b="B"; if (foo) return a; else return b;
because the ternary expression is, as the name implies, an expression. So you are converting the const char* to std::string AFTER the expression evaluation, but if you use the if-else code you have no expression to evaluate, you are just converting one literal value directly to std::string
Jason, you have to stop calling code optimal after only eyeballing the disassembly. You with your experience must have read multiple books explaining to you the fallacy inherent in assuming anything about performance without taking some measurements.
Some assembly instructions are objectively and consistently more expensive than others. Testing is necessary for more complex systems, but not everything needs testing when it's literally one cpu instruction Vs another or just objectively doing less work.
It's no matter which way is better if you have to use strict coding rules. For instance the MISRA doesn't allow to use multiple return points in functions
LOL - if every single small thing in C++ should be written by comparing its assembly materialization in various compilers so it achieves max performance (as if it was the bottleneck) why just not write it directly either in C or assembly? is productivity a factor here? or just job security for the C++ veteran? Why not use a managed language so you can finally write a simple "if" and return a string without worrying about copy elision, small string optimization, is it C++17,20,20+ and a zillion of other things including that of your code reviewer (the "C++ standard lawyer") adding another piece to your prosecution file if you failed one of those rules. Hahahaha - the slave must love his chains ...
I wonder why std::string does not have a constructor taking c-strings as input. My favorite solution in this case would be: #include template std::string to_str(const char (&str)[SIZE]) { return {&str[0], SIZE-1}; } std::string get_value(const bool b) { return b ? to_str("Hello") : to_str("World!!!"); } Considering that probably I would use 'to_str' many times.
Every compiler already has an optimization for constructing std::string objects from character literals. You would certainly get the exact same results if you simply did: return b ? std::string("Hello") : std::string("World")
There is a very definitive answer in this case. Adding the extra temporary value causes the return statement to use copy constructor of string vs move constructor of string. Copy constructor of string has the overhead of a dynamic memory allocation, whereas move does not. To make it fair, the single return path example should return std::move to bypass this overhead.
This is about C++, not C. If you try this technique too much in C you'll eventually get into trouble because C doesn't have RAII. Make no mistake - C and C++ are *very* different languages.
After reading a few comments that take the position that isn’t mine, I’ve learned that I don’t work and play well with others when it comes to writing code. Oh, well.
The problem here is, if we think for the compiler too much, the compiler itself becomes a burden. We can do just fine doing LLVM directly, or writing in MSIL, or generating it in large chunks with any high-level language we choose. And it most cases this will be the most effective approach due to the level of control we'd have. Compilers are not essential for the computing, they're only accidental. There should be some kind of balance between the complexity the compiler reduces and the complexity it creates instead. Otherwise it's just impractical to have one.
Yeah, this is what makes C++ awesome and awful at the same time. I started a journey several years ago to really fully understand what happens with my code, and how to make it as fast as possible. I've seen lots of things like this along the way. Sometimes it makes code paths start running so fast that it seems to defy the laws of physics. Other times a tiny change can cause a seemingly unrelated slowdown, when you fill up the instruction cache for example. Ultimately, for all the control we have in C++, there's much more that we can't explicitly control from the language, and instead gets controlled by whoever made the CPU, whoever made the optimiser, whoever made the memory system, etc. There is indeed zero overhead abstraction deep inside of C++, but it's not always easy to lure it out from the shadows.
Lol. I used to be enamored with C++ but not anymore. I learn this kind of stuff only when I cannot avoid it. When I write C++ I find myself thinking a lot about what the compiler might do instead of the problem I'm actually coding for.
The example given in this video is too trivial. (Most) Real world functions would not return constant values, and could have more than 2 return statements. Also having a single return point makes it easy to debug what the function is returning - just add a single breakpoint on the return. With multiple return points you would need to find each one and add a break point for each.
Good video Jason, however, I believe you're muddying the waters between Optimal/Efficient and Maintainable... You clearly demonstrate how optimal and efficient this code can be, however I would strongly argue it's not as maintainable as a single exit point solution. There are two return points in this function... That's twice as many potential tests and points of failure, more work, more things to maintain and more things for a new-comer to the code to learn about. Okay, this is a trivial example, but expand up across many more functions, each with more than one way out, with multiple conditions, states and expressions controlling the flow out and you suddenly exponentially increase the number of tests you need to write and maintain. Is this a bad thing? When targeting a product which demands the most optimal code, no. No, multiple end points are fine, they give a performance boost and achieve your product goal at the cost of more maintenance and a higher cost of entry to your code base. When targeting a product which demands low maintenance costs and ease of entry into the code base, then yes. Of course it's a problem, where you would have to look at applying structured programming technique to make the code-pathways as obvious as possible. On my personal coding standards list this means one exit point for every function. More than one exit point results in potential Spaghetti code. So when the whole purpose was to aim for maintainable code... Efficiency and Optimization at the compiler level is not really a consideration, in fact I've heard and read arguments against programmers writing code in an effort to second guess what an optimizing compiler will do (and they can argue that point much more eloquently than I). Overall, I see your points made, I like them, this is a good video, except at around that 12:58 mark, this is not the most maintainable code, you increase your exit points that automatically means more places to check for potential failure, adding more test requirements, more per-requisite tests to each point of exit. With one point of exit you can test range, test expectation and even know the flow the function must have taken all with one test, the power of this approach is to reduce points of failure, lower cost of comprehension and improve debug-ability... How many breakpoints to debug a function with one return point? Two at most, one at entry and one at exit... How many for a multiple returning function? 2 + N (where N is the number of return statements).
Because of multiple exits points there are not more tests, it's because there are if tests, which cause branching, which makes for more tests. The tests for both versions of a particular function should be exactly the same, as the function produces the same output for the same input. The value range is based on the return type and/or function contract/post condition, not the amount of return statements. As I've said in other responses, imho guard clauses clearly illustrate that multiple return statements can be more readable and maintainable. In case of the guard clause, the breakpoint thing also doesn't hold up: when you set a breakpoint at the end of a function, and the return value has some value you don't expect, you still need to step through the function, and potentially the whole function to be sure the return value doesn't change anymore. With guard clauses you'ld exist early out of the function and you don't need to look at the rest of the function.
@@frydac Totally agree. (I agree with the original comment that readability trumps optimisation, at least on the first implementation), but agree with you on multiple exits making it easier to work out what's being tested. Guards at the beginning mean you can exit early and if you get to the end of the function you know something's gone wrong and handle it appropriately. And regardless of the number of exits you'll still have to step through the code to find which condition isn't correct, but with multiple exits, at least you only have to worry about one condition at a time.
I cannot agree with that! The only correct solution cannot use std::string because using std::string is never good. If you dig a bit deeper you probably would find an optimum way to return a string_view and actually never storing it in a string.
As an assembly language programmer I have to disagree. Calling conventions and endianness aside, you should always return the length of data with dangling pointers, and I don't give a damn if it screws up your opitemizations. Just assume calling function needs to know. This is the kind of shit that got Microsoft in a bind with 94-98 alll the way to now from you C/C++ programers. The whole REASON returns were one var was because it was shoved into the only regester so fine! Have 30 more them! At this point what is EVEN my job? I am spending more time in my mind numbed N2O haze of reincarnating 20 year hardware into some god damn Raspberry PI JUST so I don't have to FPGA the bitch to find out someone already did it 20 years ago, but the code was fucking blipped out because he called another coders pants "weird" (God I love old cvs comment commits) and they stopped making the IC chip that would make my life 500% easyer 2 years ago because of COVID. Its easier to build a state machine at this point than build another cpu but now my buzz is gone so my main point. Don't worry about your shit C++ code. Its doesn't matter how garbage it looks. Its not going to compare to the pleae of shit thinking that xoring the same register DOES anything to protect you from the boggy man of not documenting your shit. Or that the optimizer SHOULD know it NEEDS to be zeroed. Unless your working in that magic 60ms time loop for games, don't bother. Make it look pretty so people like us don't have to hunt you down and gut you like a trout.
I know some people who should see this. :-) Though, the people I'm thinking of would just blame C++ and say that this is just one more example of why it's a terrible language.
well they should show me the assembler of their language, lets get them clear in this you can just ignore all this, you'll just wont get optimal code-gen (not that you'll get it in another language ... well maybe rust).
ratgr the people I'm thinking of work with the assembly level output all the time and are very concerned with performance. They are well aware of C++s strengths. ☺
ratgr - complexity, need to do your own memory management. I don't think many people have worked out that memory management systems destroy your ability to control memory layout and cache usage. I agree about the complexity bit. It feels like there's a much smaller and more elegant language inside of C++ struggling to get out.
Well, Rust stores strings as "fat strings", i.e. it doesn't reduce the string down to a pointer, but a pointer + a length. Meanwhile C++ needs the whole std::string class construction, just to store the length alongside with the string and therefore you have to care about these details. See: godbolt.org/g/nZma1i for a Rust version - notice that it stores the string length. The problem C++ has is this: If you return a const char*, you can only get the length of the string via O(n) counting. If you return a std::string, accessing the string length is now O(1), but you have to go through the std::string constructor, which generates much more assembly. Meanwhile Rust has both: O(1) access to the string length + small assembly. Secondly, Rust can annotate that the lifetime of the returned string is a compile-time constant (via 'static). For example, you could do this (stupid, but valid C++): const char* get_value() { std::string a = "Hello"; // returning pointer to function-local variable return a.c_str(); } or: const char* get_value() { return nullptr; } versus: const char* get_value() { // ok - returning pointer to constant variable return "Hello"; } Again - it's perfectly possible to replace const char* with std::string in these examples, but then you have the performance penalty of having to go through the std::string constructor, which, even with GCC, generates fairly horrible assembly compared to the const char* version. And yes, GCC catches this with -Wreturn-local-addr in these simple cases (in more complex cases it'll fail), but my point was that it is possible to compile this in the first place. To the outsider, who doesn't look at the function content, all functions have the same signature, but two of them will crash. Rust can annotate the const char* with a marker ('static) that says: This string is known at compile time and therefore always valid during the duration of the program. Also the pointer can never be nullptr or point to invalid memory. C++ can't make these kinds of guarantees, you are at the mercy of the original programmer. Last but not least, Rust also does not have implicit conversions from a const char* to a string, you have to explicitly convert it. In the example in the video, notice how the assembly varied depending just on the return type of the function - it is invisible that there is a std::string construction going on until you look at the assembly. I personally find implicit conversions very dangerous - yes, you save a bit of typing, but it can hide a huge performance bottleneck and lead to bugs. Rust is certainly not easier than C++ at first but it's very consistent, there are no huge surprises. So in the long run, I think it's easier to learn than C++. I didn't want to evangelize Rust here, but it's pretty much the only language that comes close to this efficiency. If it has a "better grammar" - well, that's in the eye of the beholder.
We know that the compiler can optimize many things. But we also know that the compiler does not know our intents at the highest levels of abstraction. This episode made me realize that we should know exactly what is the highest level the compiler can understand our intent. Because it can not optimize further than that. Also now I understand the value of what Mr. Stroustrup always says: The code should reflect our intent. On reason for that is that the compiler also understand our intent and optimize things away.
iirc, the whole "one return" policy started with the necessity to guarantee that, in C, calls to release handles to system objects are ran every time. So, yeah, in full modern Raii codebase, it no longer make sense, and you can freely avoid creation of an empty object.
Yep. Took me a while to switch over after spending so long with C coding.
It especially makes no sense if you use throw.
Yup.
It is also a MISRA C rule, and a rule I hated absolutely the most.
Not only it made the code much harder to write and read, but it also completely wrecked the cyclomatic complexity (that was also being checked...).
And the best part? Drivers we wrote didn't allocate / release any resources at runtime.
That's a practical benefit, but I think the idea also comes from a misunderstanding of something in the original "structured programming" paradigm shift some time back. It refers to "single return" but it has nothing to do with how many return statements are inside a function; rather, it has to do with where the code resumes after the function returns: it's such an alien concept today, but you could have different success/failure return points or more generally fold conditional branching on the return value into the function code itself. The closest thing today would be a function that throws a different exception to classify the result, rather than returning a value representing that classification, and you invoke it using a try block with different catch's for each case.
Finally, another practical benefit for having a single return statement is ensure that RVO is triggered, by turning it into NRVO instead which always works.
This was absolutely astounding. Thank you! I've been trying to get back into reading the asm for my instructions and this was very helpful!
Somehow the main thing I noticed is that the "90" part of "1234567890" is 12345 in little endian. Funny constants are distracting.
I'd even go a bit further (and take advantage of string literals) if I were to do it with a ternary: return b ? "Hello"s : "Worlda"s;. ^^
Very interesting ! But why letting this « else » statement in multiple return path ?
Readability and maintainability ? I do prefer multiple return path because it allows to remove lots of extra lines/indentations level. In the end, the less lines you have to deal with, the easier it is to review the code.
Just imagine how much better the world would have been had C come with a native string datatype.
I suppose that at the time a pointer to a char seemed like a neat and simple way to manipulate text that didn't require much overhead, so a native string type seemed redundant. I'm only guessing though.
... and in the finance world a native date (not a datetime a date)
The world would've been better if the Unix people just used Pascal instead.
While working on the bytecode-VM for my little programming language I noticed a big performance boost using mutiple return paths. Even in void functions.
if and return was much faster than a chain of if else. About 20-30% speed gain.
I got those results on GCC-10 with -O3 and c++20. The compiler was able optimize a lot more in this case.
I thought multiple return paths were the correct answer, but for a different reason. Doing multiple returns allows the compiler to verify that the condition is exhaustive. Now, as long as there's an "else", that's not much of an issue, but you might be tempted to have some more complicated logic in place which you think is exhaustive but it isn't. In that case the compiler would force you to reconsider and at the very least throw or assert or whatnot. In the single return path version, you might just return the default constructed (or uninitialized for PODs) value.
Some style guides eg MISRA don't like multiple returns. I don't particularly like MISRA.
I recently returned to programming in C++ after many years of not programming, and as I was developing my application I had to ponder many returns verses a single return when I had a situation that required the calculation based on many parameters. I didn't consider optimum code generation, rather for me it was more important that the code be easy to read. With a single return path, either every evaluation criteria has to be nested, (which leads to horrible levels of indentation (impossible to read and work out what is or isn't true)) or, every evaluation has to "exclude" all the conditions that have been checked but aren't true, also horrible to workout what is or isn't true.
By using multiple return paths, one condition at a time is evaluated, and if true, we know we're going to leave and never have to worry about it again in the rest of the function.
As you say, if you get to the final return, either something is wrong with the conditions to evaluate against and we can either return the default constructed object or uninitialised POD or through an exception or assert(false) or we are okay and return a harmless value.
ASCII string literal "90" is 12345 in decimal. Who knew!?!
Some of us are stuck with targeting code for RH/CenOS 7 which does not have SSO but uses the "old" string implementation in the standard library. That prevents the single-64-bit constant code from working, as it always has to allocate and copy.
I'm currently leaning to using sv suffix instead of lexical string literals, to preserve the length knowledge _in general_ when the compiler deals with const char* as strings.
This seems like the wrong conclusion. It's not that you should use independent return statements but that you should not use C-types as strings. If you use strings, why are you using single-value return statements to the implicit constructor? Isn't the conclusion instead that you should use single-value return statements if you want to rely on implicit constructors?
I like the episode, it makes a lot of sense to keep in mind that compilers are not always able to do their magic for allocating types.
My thoughts exactly. It's not about `return` at all. You get the same issue when writing:
const std::string prefix = someflag ? "value one" : "the other one";
and because of the desire to use const we are led to using the ternary operator. But this will generate bad code for the same reason.
The issues in the code are actually:
- using a computed value as the argument of a constructor, _especially_ in the case of lexical string literals where the optimizer can constexpr the strlen when the value is known at compile time;
- assigning to a default-constructed instance rather than initializing it when you are ready for it.
- ensuring NRVO vs _maybe_ getting NVO if every return statement is compatible with that.
I think the real benefit to presentations like this is that it encourages people to try their own ideas and play around in Compiler Explorer.
I don't understand! Why would the compiler ever generate a call to strlen for a string literally when the length is available at compile time?
> 12:51 _"one [line of code to a function]"_
won't that increase the function calls and calling records on stack? I genuinely wanna know ur thoughts!
The book chtp-6e (quite old) or c++htp-8e by deitel-deitel always emphasize on reducing function calls for small things.
Not with return value optimization. Also - compilers are very very good at inlining and eliminating small function calls.
(0:22 in)
I prefer multiple return paths only IF the conditions are simple and it allows me to avoid nesting a whack of if statements or more than a depth 1 loop break (or using status variables to force multiple loop breaks).
if (A) { ...} else { if (B) {...} else {...} } is much less nice than if (A) {... return} if (B) {....return} C.
I love your videos a lot (and have only recently got back into C++ after a 10 year hiatus, so I'm trying to catch up on C++ 11 / 14 / 117) and you have taught me a lot of fascinating things, but in some cases, I think that clear, easily read code with less nested blocks, while possibly being less efficient, is preferable to the hyper-efficient but less readable code. Many C++ programmers are obsessed with optimized code, which often isn't necessary, and this is coming from someone who has a PhD in combinatorial design theory where even small optimizations can make the difference between tun times in hours vs. days.
This actually made me go back to programming school. Really appreciate your time and efforts.
at 8:00 wouldn't return std::string(b?"Hello":"World"); also work? and it would be easier to read/write I think.
Not as well as you'd expect, because you have to consider the result type of the ternary expression. It's a `const char *` as opposed to a `const char []` (which is what "Hello" is) so the compiler is not able to optimize in the same way as it can with two completely different return statements.
I couldn't really follow the difference between using the ternary operator and the if else clause, other than some internal compiler logics. From my point I "see" an implicit else clause in the ternary operator, it should in my mind be the same.
I see comments below mentioning the difference that the ternary operator is more of an atomic "macro" like operation rather than an "evaluation" option, in that case I would consider the ternary operator to be questioned in all possible optimisation cases.
The ternary operator with string literals is treated as an expression with temporary type `const char*`, which does not include length. That const char* is then used to construct a std::string, so in the general case where the two result operands could be a const char* created at runtime from a function call, it would not be optimised. I don't know why it isn't optimised in this video tho
I agree with this completely, and I'm gratified to come across a like-minded soul. The "single entry, single exit" rule is an ancient, outmoded practice that hails from the Fortran days. If early returns are a problem for you, then I would submit (as Jason more-or-less does) that your coding style has other, worse problems. I would further contend that if minimizing time to market and programmer pain is a higher priority for you than maximizing performance, then you shouldn't be working in C++ at all. All that said, it's not my impression that this post was intended to make any recommendation concerning how to approach writing production code, but rather merely to point out which of these idioms is more optimal as far as performance goes.
Very interesting and informative post.
What do you think of the "my string"s (string literals) method for creating an std::string?
I would have used something like:
std::string getValue(const bool b)
{
using namespace std::literals;
return b ? "Hello"s : "World"s;
}
Should be the same as `return b ? std::string("Hello") : std::string("World")`.
It would be interesting to explore this concept with other return types….not sure the general recommendation “prefer multiple return paths” is optimal in other cases (including switch statements). For return types that fit into registers I would guess perf would be comparable for either multiple/single path. Also this simple example is a bit contrived since real world scenarios typically have a chunk of “other stuff” going on in the additional paths.
For trivial objects of any type or size this example becomes irrelevant. The reason this example is not contrived is because we rarely think about trying to make as much of our API trivial as possible. Also, yes, you can *almost always* refactor your code to look like this by composing multiple functions, which can result in much more optimizable code.
See also: ruclips.net/video/ZxWjii99yao/видео.html
What does it mean for compiler to "see through"? Why it cannot "see through" in the first example?
6:18 > _"disassembly"_
u meant assembly right? or disassembly of the c++ code? or both ;)
Why not making the return type of the get_string() function a string_view? You only get the string anyway.
Need to watch out for impacts on branch prediction going from one ret to two.
en.wikipedia.org/wiki/Branch_predictor#Prediction_of_function_returns
I somehow fail to see how the ternary operator differs from a simple if-else, good video though!
A ternary isn't a branch in execution, but rather a value. An if/else leads to 2 different paths where different instructions are executed. A ternary represents one of the 2 values on its right side.
So an if/else is different stuff to do.
The ternary IS the string or integer or whatever you put in there.
Ternary evaluates to a const char *, which then must be used to initialize a string, but since the const char * came from a ternary it's length is not known hence there's extra work to do.
@@hex7329 The ternary makes the const char* a calculated thing, so the compiler no longer knows the length without calling strlen.
The if/else constructs a different string on each branch.
It's not that if/else is different per-se, but you are also doing different things in the branches in each case. The ternary would be equivilent if you put a std::string in each branch of it rather than factoring it out and constructing the string around the result.
I was wondering why you didn't show with MSVC too. I did try and ... it was horrible: about 20x times more assembly lines generated for no reason :o
I think thise series consider only serious production compilers :p
I think you just answered your own question... also keep in mind that fiddling with optimization flags can give radically different output.
heaving return type as const std::string_view would be better replacement for cont char*, since you only need return "literal"; without std::string("literal")
Ofcourse if you want to mutate the returned string and you do not really care about heap allocation, then return should be std::string.
Do not return either string_view neither char*. It's bare open code for dandling pointer. string_view is only useful as an argument, if any.
@@tatoute1 right, but I am only saying that if you necessarily have to return const char*, rather use std::string_view since it can be used safely e.g. over range loops
@@tatoute1 not exactly, strings stored in the binary don't need to be copied and moved to the heap, so returning std::string_view is a better decision
I tend to mix a little bit. If I don't have all the info I need till later I might create a ret variable to have something to write data too as I'm reading it. But when I have a branching path where it's like. Is the data compressed or not? Then i'll use multiple return paths. I'm not sure how correct this all is but that's how things are. Thank you for all these videos.
Great episode. It is easy to not think about what the compiler will do at all. Tho no every programmer will have an encyclopedic (and updated) knowledge of every compiler to know these things from hearth when in doubt you can at least check the generated assembly. ^_^ if (b) return "foo"; return "bar"; //ftw
Best way to return a string-like object perhaps, but lots of functions return primitive types and POD. Title should be be narrowed in scope.
It is impressive to see the code generated by the compiler when you add std::string. The code is much less long.
Jason would you be able to specify the compiler version since the videos would be dated after a while?
You can see the version at the bottom of the windows. GCC 9.0.0 and Clang 7.0.0 here.
There is also an opinion, that having multiple return statements can cause RAII bloat, with each statement generating a different destruction code (because of the scope it is being in).
That is interesting. If so does the destructor change the instruction binary according to the context it is being called with (e.g. data members change inside scope)? That would then be a compile time optimization and lead to mlre performant code (with the caveat that it bloats the binary)
why did the compiler not do NRVO on the version with a single return statement ? I thought C++17 guaranteed copy elision ?
Try returning a const char* instead of a std::string which is way overengineered for the problem. Just because you can use STL instead of C does not mean you always should.
No you absolutely always should. In fact you shouldn't even use C++, use Rust. But if you have to use C++ for the love of god avoid raw pointers like the plague
@@TheMrKeksLp Rust is dead. C++ 20 is King, and raw pointers are fine for me. I don't have problems with them, and I certainly don't need to wrap up every piddling constant string in a class.
@@TheMrKeksLp borrow checking is not Free
Why don't u use *using namespace std*?
I'm disappointed that the compiler can't figure this out.
I know it's easy to have a strong opinion on how compiler writers do their job. At the same time however it is difficult to defend or advocate the virtues of c++ because of stuff like this.
We have to do better. The language is complex enough without having to concern one self with gaming the compiler.
Most people don't care, if you do care about top performance you would look at the assembly anyway.
I have some strong opinion around this topic since I have an instructor in my university forbid us using multiple returns. The reason is basically "this is a best practice that my instructor forced me to do." If her argument was something more reasonable like resource management concern, then I will at least try to debate.
Many industry coding standards, MISRA, AUTOSAR etc mandate a single entry single exit to functions.
About 35 years ago I was in my first year of a Chemical Engineering degree and we had 1 lecture a week on programming. Because the University has a computing science faculty, we were taught the CompSci approved language for programming, which was PASCAL. At the end of the academic year we had a short cause on FORTRAN because that was the computing language that industry actually uses. And a few years later, I did begin using FORTRAN for my job, and have never heard of anyone in industry using PASCAL, (although IEC 61131-3 Structured Text for programming programmable logic controllers is quite close to it).
Tip, if you every want to upset an instructor, just prefix a comment on their course with "In the real world".
Of *course* it shouldn't be `const char*`. You may or may not need to put it into a `string` right away, but if you don't want to do that you should at least use a `string_view`.
Count me as someone that prefers an implicit else. ;^} I’m generally more concerned about understanding the context of the multiple conditions and structuring the returns such that the most probable condition returns first. It’s true that sometimes you won’t know... At which point it’s necessary to break out the tape measure (if you can ever find it).
Same. I find it weird that the folks at clang think this is a problem for readability - I find it to be the opposite.
clang.llvm.org/extra/clang-tidy/checks/readability-else-after-return.html
@@superscatboy he said implicit, as in, implied but not explicitly there. So I think he means he prefers clangs style, and I do as well for the record. It reduces visual noise, particularly in the case of return values and so on. 😉
This video was suggested to me exactly one year after it came out. Sounds like that must be part of the RUclips algorithm, or something.
That else has no reason to exist in the multi return except to have extra indentation. This example it does not really matter though. I like the multi returns. I remember professors pushing for the one return method.
the else doesn't matter ... except when it does.
Example: if constexpr, if consteval, and older gcc compilers in the case of a constexpr function that just happens to have an if statement in it.
I always use the else, it makes me happy and its consistent with my code.
Looks like a great plug for Compiler Explorer :) :) :)
Yes, I'm surprised he doesn't have to flag it as "product placement" in Europe. :)
How did we get by without compiler explorer?
objdump
The always correct answer is: "follow your company's style guide." But if you have a choice, do what makes for the most readable code.
If your compiler makes a sub-optimal job of it and you actually care, change your compiler.
"follow your company's style guide", no, fight stupidity in style guide. If the style guide choose between CameCase or use_underscore, fine. If the style guide is about the number of return or the length of the function, contest it. Mandatory things like this is bullshit stupidity, created by morons, just to find something to says in code reviews.
Here is the ultimate code style:
- make your code understandable
- use whatever tools the compiler offer you to reduce the probability of bugs (ex make everything const except if not possible).
- do not use new forms/things/syntax just because it exists. If you want to experiment, do it outside of production code.
awesome video..& videos :) I have a question ; is it safe to return a string_view from a function looking at the example in the video. Does not the value that string_view will be destroyed ? Just asking to learn...
Do not return string_view. String view borrow the string content somwhere and offer no garanties about this somewhere lifetime. It's open bar for dandling pointer.
It is safe for string literals like in the example.
std::string_view f() {
// Safe
return "hi";
// p points to a string literal
auto *p = "fine";
return p;
// Unsafe, returns pointer to local storage
return std::string{"bye"};
std::string s{"bad"};
return s;
}
So just don't ask the compiler to default-construct your retval;
std::string get_value(bool b)
{ //consider b==false most of the time
std::string retval{"Hello"};
if(b) //sometimes its true
retval = std::string{"Worlds"};
return retval;
}
None of this is the compiler’s fault. You need to put the “static“ keyword in front of your function prototypes to get the optimizations you want.
Hey, thanks for this insight. This was realy helpful.
I tried constexpr but compiler warn about converting string litterals to char *
I assume your declaration was something like `constexpr char* blahblah();`? If so the return type is `char*`, not `const char*`.
You're marking the function as `constexpr`, not the return value. In that context, it wouldn't make sense either. And indeed, conversion from a string literal `"hello"` to a non-`const` `char*` is illegal in C++, hence the warning.
this isn't really a general rule tho? what I i'm not using std::string? heck what if not using any constructors?
Understanding where and when return value optimization comes into play, when it is required, and structuring your code such that you take advantage of it, matters for anything that is bigger than about 2 registers.
@@cppweekly it only matters if you're using constructors/destructors
I'm a bit confused and surprised what RVO is working in this case. It's two returns with condition on argument.
When using multiple returns within a function, it might be useful to discuss the impact of RVO when highlighting "optimized" techniques.
Hallo, what is your opinion about using continue and break in loops?
I tried Compiler Explorer with gcc 8.2 and -O3 flag and following code:
#include
void calc(){
for(unsigned int i = 0; i < 1000; i++){
if(i%2!=0) continue;
std::cout
benchmark it. also try if using likely/unlikely on the if expression changes the ASM emitted.
Interesting insights, but I also think you are putting too much emphasis on optimization. In general, one should prefer code that expresses the intent most clearly and succinctly, which in this simple case is by the ternary. Compilers are getting smarter all the time, and unless it is really necessary, one should not write code that is harder to read to accommodate a compiler that will be dumb by tomorrow's standards.
As for the return style, I think the disconnect is that people that argue for single-return are thinking about functions that are much too long and complex to begin with. I prefer multiple returns, structured to first get any preconditions out of the way one by one, with the actual work at the bottom of the function. But these functions are hardly ever longer than half a screen (at 80 columns), so I'm never confused as to where all my returns sit (also helped by syntax highlighting and a blank line after each return).
People have been saying for decades that smarter compilers will take care of it. While they are getting smarter, they're just never quite there. Looking at generated code is still often a nightmare. It's okay if you aren't doing that much work. But if you want or need your code to be lean, it can be helpful to be aware of patterns a compiler can or can't turn into something good. I'd recommend not overfitting your style to a particular compiler, but choosing constructs that translate to good machine code with few transformations. That should work on a range of compilers and not be too hard on you.
I agree multiple returns can be clean and readable. Speaking more generally, after carefully splitting code paths based on preconditions, don't make them join again unless there's commonality on what needs to happen next. Obsessive joining only makes it hard to tell when a particular condition is handled and done. Paths applying to many conditions are more complex and bug-prone. Anything could potentially still happen, either by design or by bug. You won't know without analyzing the entire function.
I guess there is a performance lesson to take away from this video, but it's not about return style at its core. The real key seems to be that object construction from literals should be done explicitly and straight from the literal, because a compiler is most likely to take shortcuts with a source literal that is immediately visible as a constructor argument. If the value used for initialization is obscured even by just very simple logic (such as a ternary) before going into the constructor, these optimizations might already fail to kick in.
Returning literals from a function by multiple return is how you get literals into the implied return type constructor directly, but that is just one (albeit clever) application of a larger principle.
He kind of took the wrong path, but still got to the right conclusion :-)
Honestly if you're programming in C++, you should probably be worried about performance, especially in your functions you expect to get called a lot.
This version seems to generate considerably less assembler code on VC++
return c_STRS[b_?0:1];
Can you comment about this?
-11:50-- i think all of people would prefer longer videos over 5-6 min video.-
I'm wrong. however i would still like it if you could do a longer video once a in a while.
That's a shame. I certainly prefer longer videos, but if thats what the stats say then you have no real incentive for the extra effort. If you feel like experimenting, you could try longer ones for Patreon supporters, while using short ones for open youtube as patreon bait (A 5 on the scale from 0 to Bryan Lunduke).
Also I'd like to see some more shout outs for CppCast. I've been following you for quite some time but only came across the podcast last week. It's a great listen!
I knew I had seen the name somewhere! Eh well, fool on me for not checking it out earlier. Though I keep a close eye on the std proposals forum, I'm somewhat removed from the progress further down the pipeline, and the podcast is a great way of keeping up with proposals, papers, notable opinions and viewpoints from developers who work in niche areas.
Interresting stuff! Thanks
Could you do a video about inline and differences between inline in method declaration vs inline in method definition? I've always thought inline in definition inlines methods body to the class' body while inline in declaration suggests the compiler to replace function calls with function's body. Also I could never come up with an example to show the advantages of `inline` in Compiler Explorer
inline has nothing at all to do with inlining a function
wow, now I am even more confused
General video concerning inlining and compiler optimisations would be nice. I got an impression that lately inline doesn't do much. Just little tip for a compiler. In my first compilers it meaned that it will inline that function.
reconn it used to have a *lot* to do with whether or not a compiler would inline something. A little like the now deprecated (or maybe actually removed?) register keyword that hinted to the compiler that it ought to keep a variable in a register.
Right, but register is as frequent as a goto nowadays yet I still see inline in modern code strangely enough. Wonder if people know what it does.
Any plans to add Deep learning to the compiler?
MISRA and AUTOSAR requires to have only one return statement per function placed at the end of the function.
Vendicar Kahn may be you don’t. But they are doing their job to ensure safety of your next ride...
One more possibility:
return std::string(b ? "Hello" : "World");
I checked it will still call strlen. using gcc compiler
@@mikezhu5894 : Yes maybe so. But that is a quality of implementation of the compiler. There is a std::string constructor which takes a char *, a size and an allocator. The compiler knows the string length of either option. So it could easily optimise a call to a string constructor of 3 parameters, losing the 1st parameter in some form of conditional move. And after this do further optimisation of the constructor so it is still a string but it is constructed inline.
So yes, compilers can go further with this construction as Jason noted in his clip.
This function is trivially short and does not reveal how to work with the compiler to make it optimise the return value. You are not returning literal strings in real code.
Why not something like this?
It shall be as fast as returning a const char *, if im not wrong.
const std::string & get_value(const bool b)
{
static const std::array strings = {
"1234567890",
"qwerty"
};
return (strings[b]);
}
Edit: my remark is more about the return type and the static string than the array trick (it was faster to type than the if/else variant)
I've recently started doing static constexpr char* to define all my return arrays. Then, my if else, or switch case, and return the char* by name. Is that equivalent to your suggestion? E.g.
static constexpr const char* a="A";
static constexpr const char* b="B";
if (foo) return a;
else return b;
Your static seems to be superfluous?
Can someone explain why the ternary operator returns a const char* and then it changes it to the string?
because the ternary expression is, as the name implies, an expression. So you are converting the const char* to std::string AFTER the expression evaluation, but if you use the if-else code you have no expression to evaluate, you are just converting one literal value directly to std::string
Jason, you have to stop calling code optimal after only eyeballing the disassembly. You with your experience must have read multiple books explaining to you the fallacy inherent in assuming anything about performance without taking some measurements.
Some assembly instructions are objectively and consistently more expensive than others. Testing is necessary for more complex systems, but not everything needs testing when it's literally one cpu instruction Vs another or just objectively doing less work.
Wow this is cool. It's mostly gone over my head since I do very little c family programing or even programing in general, but it's still neat.
What name os/distro you using ?
I thought multiple return paths prohibited RVO though?
It's no matter which way is better if you have to use strict coding rules. For instance the MISRA doesn't allow to use multiple return points in functions
return (void)"Everything is fine.";
LOL - if every single small thing in C++ should be written by comparing its assembly materialization in various compilers so it achieves max performance (as if it was the bottleneck) why just not write it directly either in C or assembly? is productivity a factor here? or just job security for the C++ veteran? Why not use a managed language so you can finally write a simple "if" and return a string without worrying about copy elision, small string optimization, is it C++17,20,20+ and a zillion of other things including that of your code reviewer (the "C++ standard lawyer") adding another piece to your prosecution file if you failed one of those rules. Hahahaha - the slave must love his chains ...
I wonder why std::string does not have a constructor taking c-strings as input.
My favorite solution in this case would be:
#include
template
std::string to_str(const char (&str)[SIZE]) {
return {&str[0], SIZE-1};
}
std::string get_value(const bool b) {
return b ? to_str("Hello") : to_str("World!!!");
}
Considering that probably I would use 'to_str' many times.
Every compiler already has an optimization for constructing std::string objects from character literals.
You would certainly get the exact same results if you simply did:
return b ? std::string("Hello") : std::string("World")
Very interesting subject here... We return from functions so casually but never knew what was the performance impact of each approach
There is a very definitive answer in this case. Adding the extra temporary value causes the return statement to use copy constructor of string vs move constructor of string. Copy constructor of string has the overhead of a dynamic memory allocation, whereas move does not. To make it fair, the single return path example should return std::move to bypass this overhead.
This good content. Precisely the reason to love C /C++. So much scope for compile optimization.
This is about C++, not C.
If you try this technique too much in C you'll eventually get into trouble because C doesn't have RAII. Make no mistake - C and C++ are *very* different languages.
"If you wanted a string, you should have put a (st)ring on it" 🎶🎵
After reading a few comments that take the position that isn’t mine, I’ve learned that I don’t work and play well with others when it comes to writing code. Oh, well.
The problem here is, if we think for the compiler too much, the compiler itself becomes a burden. We can do just fine doing LLVM directly, or writing in MSIL, or generating it in large chunks with any high-level language we choose. And it most cases this will be the most effective approach due to the level of control we'd have. Compilers are not essential for the computing, they're only accidental.
There should be some kind of balance between the complexity the compiler reduces and the complexity it creates instead. Otherwise it's just impractical to have one.
Yeah, this is what makes C++ awesome and awful at the same time. I started a journey several years ago to really fully understand what happens with my code, and how to make it as fast as possible. I've seen lots of things like this along the way. Sometimes it makes code paths start running so fast that it seems to defy the laws of physics. Other times a tiny change can cause a seemingly unrelated slowdown, when you fill up the instruction cache for example. Ultimately, for all the control we have in C++, there's much more that we can't explicitly control from the language, and instead gets controlled by whoever made the CPU, whoever made the optimiser, whoever made the memory system, etc. There is indeed zero overhead abstraction deep inside of C++, but it's not always easy to lure it out from the shadows.
Lol. I used to be enamored with C++ but not anymore. I learn this kind of stuff only when I cannot avoid it. When I write C++ I find myself thinking a lot about what the compiler might do instead of the problem I'm actually coding for.
@@longhaulblue1145 So how is writing in another language better? If you just write non-optimal C++ won't it be at least as fast as the other language?
drag and drop
I thought multiple return paths inhibited copy elision. Clearly need to have a bit more of a play with this
Wow!
The example given in this video is too trivial. (Most) Real world functions would not return constant values, and could have more than 2 return statements. Also having a single return point makes it easy to debug what the function is returning - just add a single breakpoint on the return. With multiple return points you would need to find each one and add a break point for each.
Good video Jason, however, I believe you're muddying the waters between Optimal/Efficient and Maintainable... You clearly demonstrate how optimal and efficient this code can be, however I would strongly argue it's not as maintainable as a single exit point solution. There are two return points in this function... That's twice as many potential tests and points of failure, more work, more things to maintain and more things for a new-comer to the code to learn about.
Okay, this is a trivial example, but expand up across many more functions, each with more than one way out, with multiple conditions, states and expressions controlling the flow out and you suddenly exponentially increase the number of tests you need to write and maintain.
Is this a bad thing?
When targeting a product which demands the most optimal code, no. No, multiple end points are fine, they give a performance boost and achieve your product goal at the cost of more maintenance and a higher cost of entry to your code base.
When targeting a product which demands low maintenance costs and ease of entry into the code base, then yes. Of course it's a problem, where you would have to look at applying structured programming technique to make the code-pathways as obvious as possible. On my personal coding standards list this means one exit point for every function.
More than one exit point results in potential Spaghetti code.
So when the whole purpose was to aim for maintainable code... Efficiency and Optimization at the compiler level is not really a consideration, in fact I've heard and read arguments against programmers writing code in an effort to second guess what an optimizing compiler will do (and they can argue that point much more eloquently than I).
Overall, I see your points made, I like them, this is a good video, except at around that 12:58 mark, this is not the most maintainable code, you increase your exit points that automatically means more places to check for potential failure, adding more test requirements, more per-requisite tests to each point of exit. With one point of exit you can test range, test expectation and even know the flow the function must have taken all with one test, the power of this approach is to reduce points of failure, lower cost of comprehension and improve debug-ability... How many breakpoints to debug a function with one return point? Two at most, one at entry and one at exit... How many for a multiple returning function? 2 + N (where N is the number of return statements).
Because of multiple exits points there are not more tests, it's because there are if tests, which cause branching, which makes for more tests. The tests for both versions of a particular function should be exactly the same, as the function produces the same output for the same input. The value range is based on the return type and/or function contract/post condition, not the amount of return statements. As I've said in other responses, imho guard clauses clearly illustrate that multiple return statements can be more readable and maintainable. In case of the guard clause, the breakpoint thing also doesn't hold up: when you set a breakpoint at the end of a function, and the return value has some value you don't expect, you still need to step through the function, and potentially the whole function to be sure the return value doesn't change anymore. With guard clauses you'ld exist early out of the function and you don't need to look at the rest of the function.
@@frydac Totally agree. (I agree with the original comment that readability trumps optimisation, at least on the first implementation), but agree with you on multiple exits making it easier to work out what's being tested. Guards at the beginning mean you can exit early and if you get to the end of the function you know something's gone wrong and handle it appropriately. And regardless of the number of exits you'll still have to step through the code to find which condition isn't correct, but with multiple exits, at least you only have to worry about one condition at a time.
I cannot agree with that! The only correct solution cannot use std::string because using std::string is never good. If you dig a bit deeper you probably would find an optimum way to return a string_view and actually never storing it in a string.
As an assembly language programmer I have to disagree. Calling conventions and endianness aside, you should always return the length of data with dangling pointers, and I don't give a damn if it screws up your opitemizations. Just assume calling function needs to know. This is the kind of shit that got Microsoft in a bind with 94-98 alll the way to now from you C/C++ programers. The whole REASON returns were one var was because it was shoved into the only regester so fine! Have 30 more them!
At this point what is EVEN my job? I am spending more time in my mind numbed N2O haze of reincarnating 20 year hardware into some god damn Raspberry PI JUST so I don't have to FPGA the bitch to find out someone already did it 20 years ago, but the code was fucking blipped out because he called another coders pants "weird" (God I love old cvs comment commits) and they stopped making the IC chip that would make my life 500% easyer 2 years ago because of COVID. Its easier to build a state machine at this point than build another cpu but now my buzz is gone so my main point.
Don't worry about your shit C++ code. Its doesn't matter how garbage it looks. Its not going to compare to the pleae of shit thinking that xoring the same register DOES anything to protect you from the boggy man of not documenting your shit. Or that the optimizer SHOULD know it NEEDS to be zeroed. Unless your working in that magic 60ms time loop for games, don't bother. Make it look pretty so people like us don't have to hunt you down and gut you like a trout.
+1 - very good episode. But I still think const bool instead of just bool is unnecessary noice here.
MISRA forbids multiple returns.
isnt this c++?
I know some people who should see this. :-) Though, the people I'm thinking of would just blame C++ and say that this is just one more example of why it's a terrible language.
well they should show me the assembler of their language, lets get them clear in this you can just ignore all this, you'll just wont get optimal code-gen (not that you'll get it in another language ... well maybe rust).
ratgr the people I'm thinking of work with the assembly level output all the time and are very concerned with performance. They are well aware of C++s strengths. ☺
Then why is it a horrible language?, is there one that would do the same assembly with a better grammar/syntax? (i'm not sure i like rust syntax more)
ratgr - complexity, need to do your own memory management. I don't think many people have worked out that memory management systems destroy your ability to control memory layout and cache usage. I agree about the complexity bit. It feels like there's a much smaller and more elegant language inside of C++ struggling to get out.
Well, Rust stores strings as "fat strings", i.e. it doesn't reduce the string down to a pointer, but a pointer + a length. Meanwhile C++ needs the whole std::string class construction, just to store the length alongside with the string and therefore you have to care about these details.
See: godbolt.org/g/nZma1i for a Rust version - notice that it stores the string length.
The problem C++ has is this: If you return a const char*, you can only get the length of the string via O(n) counting. If you return a std::string, accessing the string length is now O(1), but you have to go through the std::string constructor, which generates much more assembly. Meanwhile Rust has both: O(1) access to the string length + small assembly.
Secondly, Rust can annotate that the lifetime of the returned string is a compile-time constant (via 'static). For example, you could do this (stupid, but valid C++):
const char* get_value() {
std::string a = "Hello";
// returning pointer to function-local variable
return a.c_str();
}
or:
const char* get_value() { return nullptr; }
versus:
const char* get_value() {
// ok - returning pointer to constant variable
return "Hello";
}
Again - it's perfectly possible to replace const char* with std::string in these examples, but then you have the performance penalty of having to go through the std::string constructor, which, even with GCC, generates fairly horrible assembly compared to the const char* version.
And yes, GCC catches this with -Wreturn-local-addr in these simple cases (in more complex cases it'll fail), but my point was that it is possible to compile this in the first place. To the outsider, who doesn't look at the function content, all functions have the same signature, but two of them will crash.
Rust can annotate the const char* with a marker ('static) that says: This string is known at compile time and therefore always valid during the duration of the program. Also the pointer can never be nullptr or point to invalid memory. C++ can't make these kinds of guarantees, you are at the mercy of the original programmer.
Last but not least, Rust also does not have implicit conversions from a const char* to a string, you have to explicitly convert it. In the example in the video, notice how the assembly varied depending just on the return type of the function - it is invisible that there is a std::string construction going on until you look at the assembly. I personally find implicit conversions very dangerous - yes, you save a bit of typing, but it can hide a huge performance bottleneck and lead to bugs.
Rust is certainly not easier than C++ at first but it's very consistent, there are no huge surprises. So in the long run, I think it's easier to learn than C++. I didn't want to evangelize Rust here, but it's pretty much the only language that comes close to this efficiency. If it has a "better grammar" - well, that's in the eye of the beholder.
This is why I hate c++
So easy to do the wrong thing its driving me crazy