C++ Weekly - Ep 154 - One Simple Trick For Reducing Code Bloat
Vložit
- čas přidán 10. 02. 2019
- ☟☟ Awesome T-Shirts! Sponsors! Books! ☟☟
Upcoming Workshop: Understanding Object Lifetime, C++ On Sea, July 2, 2024
► cpponsea.uk/2024/sessions/und...
Upcoming Workshop: C++ Best Practices, NDC TechTown, Sept 9-10, 2024
► ndctechtown.com/workshops/c-b...
T-SHIRTS AVAILABLE!
► The best C++ T-Shirts anywhere! my-store-d16a2f.creator-sprin...
WANT MORE JASON?
► My Training Classes: emptycrate.com/training.html
► Follow me on twitter: / lefticus
SUPPORT THE CHANNEL
► Patreon: / lefticus
► Github Sponsors: github.com/sponsors/lefticus
► Paypal Donation: www.paypal.com/donate/?hosted...
GET INVOLVED
► Video Idea List: github.com/lefticus/cpp_weekl...
JASON'S BOOKS
► C++23 Best Practices
Leanpub Ebook: leanpub.com/cpp23_best_practi...
► C++ Best Practices
Amazon Paperback: amzn.to/3wpAU3Z
Leanpub Ebook: leanpub.com/cppbestpractices
JASON'S PUZZLE BOOKS
► Object Lifetime Puzzlers Book 1
Amazon Paperback: amzn.to/3g6Ervj
Leanpub Ebook: leanpub.com/objectlifetimepuz...
► Object Lifetime Puzzlers Book 2
Amazon Paperback: amzn.to/3whdUDU
Leanpub Ebook: leanpub.com/objectlifetimepuz...
► Object Lifetime Puzzlers Book 3
Leanpub Ebook: leanpub.com/objectlifetimepuz...
► Copy and Reference Puzzlers Book 1
Amazon Paperback: amzn.to/3g7ZVb9
Leanpub Ebook: leanpub.com/copyandreferencep...
► Copy and Reference Puzzlers Book 2
Amazon Paperback: amzn.to/3X1LOIx
Leanpub Ebook: leanpub.com/copyandreferencep...
► Copy and Reference Puzzlers Book 3
Leanpub Ebook: leanpub.com/copyandreferencep...
► OpCode Puzzlers Book 1
Amazon Paperback: amzn.to/3KCNJg6
Leanpub Ebook: leanpub.com/opcodepuzzlers_book1
RECOMMENDED BOOKS
► Bjarne Stroustrup's A Tour of C++ (now with C++20/23!): amzn.to/3X4Wypr
AWESOME PROJECTS
► The C++ Starter Project - Gets you started with Best Practices Quickly - github.com/cpp-best-practices...
► C++ Best Practices Forkable Coding Standards - github.com/cpp-best-practices...
O'Reilly VIDEOS
► Inheritance and Polymorphism in C++ - www.oreilly.com/library/view/...
► Learning C++ Best Practices - www.oreilly.com/library/view/...
ChaiScript: chaiscript.com - Věda a technologie
It's 9 months since this video was posted and I just tried the different examples using Compiler Explorer. The results were quite different from what was shown here. Generally, x-84-64 gcc (trunk) was much better that clang trunk at optimizing the code in all cases. Declaring a destructor added 3 instructions vs. not declaring one (26 vs 29. For x86-64 clang (trunk), declaring a destructor actually reduced code by 7 instructions vs not declaring one (83 vs 90). This is with using std::string and two emplace_back calls. With the destructor also defined, gcc produces 40 lines vs clang's 106.
Declaring or not declaring a destructor had varying impacts, depending on the data types being used. For int, gcc generated 10 lines fewer with a destructor declared vs not (26 vs 36). Clang is the other way around. W/o a destructor declared, it generates 28 lines. With it declared, it generates 71 lines.
It's difficult to make a pat rule about whether or not to declare a destructor. Thus far, though, also defining one will add extra instructions. But the choice of compiler seems to be far more important than the declare/don't declare of the destructor.
BTW, ~S() = default produces the same instruction count as not declaring ~S() at all.
Funny where I used to work all the cpp gurus demanded all objects have destructors regardless of use
Visual Studio, when adding a new class, automatically creates an empty constructor and destructor in the .cpp file and I might sometimes forget to delete it. Thanks for the tip.
Weird, it never does that for me. Maybe there's a way to turn that behaviour off?
Thank you Jason Turner
wow.. i actually tried this on our company's codebase a few weeks ago. It really really made a difference in the size of executable. Very true.
Dude, if I had a penny every time I pointed this out in a code review...
Great tip! Maybe removing an empty destructor or even a trivial one should probably qualify as a compiler optimization?
But what about having default move ctor and move assign? Then it's all movable again and then it shouldn't be a problem? Or is that still a problem? I make a habbit of providing at least 1 ctor (could be default), copy +move ctor (default or delete), copy+move operator (default or delete) and 1 dtor (pretty much always default)... Would I still run into this? Because if I allow for default move it should be able to figure this out right?
6:35 I've seen this exact pattern in a code written by someone who claimed that compiler can't be as fast with "smart" pointer as with raw pointer. He implemented the "smart" himself, of course, and added the nulling in the d-tor. ;-) Why???///
That Heap Allocation eLlision Optimization (HALO) is really nice in situations like this. It's something I hope the gcc devs give serious consideration to.
what about ~() = default; ?
Doesn't that implicitly delete the move ctor/assignment?
It disables it anyway stackoverflow.com/questions/33957037/does-a-default-virtual-destructor-prevent-compiler-generated-move-operations
That's fine as long as the destructor is explicitly-defaulted at its first declaration. If you do it later, it's considered user-provided and therefore no longer trivial.
Also, the destructor can't be virtual, the class's direct base classes must have trivial destructors, and the class's non-static class-type data members must have trivial destructors. The person in the stackoverflow answer doesn't know what they're talking about, and the code in the original question violates the virtual requirement for a trivial destructor.
References:
eel.is/c++draft/class.dtor#6
eel.is/c++draft/dcl.fct.def.default#5
@r00x its not always pointless, e.g. if you have forward declarations
Thanks for the video! I'm wondering why there is a difference in generated code between having no constructor, and having a constructor declared as `~S() = default;`. Shouldn't these cases be more or less equivalent?
`~S() = default;` seems to be redundant for me, but I could be wrong. In my understanding, `= default;` simply is used to "reactivate" default definitions implicitly deleted otherwise and don't really change their behavior otherwise. I could be wrong though, I know this part of C++ a little less.
it seems to have the seme effect as ~S(){}
@slipcurve The example in Jason's video: godbolt.org/z/Kpxsu4; this emits 231 lines of assembly without, and 328 lines with `~S() = default;`, using x86-64 clang (trunk). Similar behavior with GCC.
@@kmhofmann Defaulting the destructor for the int case doesn't change the generated code (godbolt.org/z/YrpXo7 ), but doing so for the string case generate the same code as inlining the destructor (godbolt.org/z/UYl3xp )
What if you use =default for the destructor?
I haven't watched it yet but the title caught my eye. I need a welcome break from debugging a compile error in a generic lambda passed to std::visit inside a SFINAE'd template function inside a templated class.
I'll trade a compile error for a runtime error every time. No matter what kind.
dtors are always noexcept unless you make them noexcept(false) or the implicitly generated one is noexcept. Hense why I assume dtors are always noexcept
About 7:14, aren't destructors already implicitly noexcept?
Is there a way to omit the empty destructor in case of polymorphic objects? Perhaps I'm not remembering it right, but if I do so I remember that I always had to explicitly declare a virtual destructor, even if it was empty.
PS: Aren't destructors noexcept by default?
Often you can find clear and comprehensive explanation at cppreference:
"As with any implicitly-declared special member function, the exception specification of the implicitly-declared destructor is non-throwing unless the destructor of any potentially-constructed base or member is potentially-throwing. In practice, implicit destructors are noexcept unless the class is "poisoned" by a base or member whose destructor is noexcept(false)."
Source: en.cppreference.com/w/cpp/language/destructor
I guess, the only way ~() = default. Default generated destructors are noexcept only if all members' destructors are noexcept. Custom destructors shall explicitly be marked noexcept if they are
@@Vasily.Vasilyev Thanks!
so base on the example; this doesnt take account cases of virtual destructors and heritance. right ?
Makes no difference.
If you are writing a base class, it must be virtual, say so and declare virtual ~Base() = default; but only if the compiler generated destructor does the right thing.
If you are writing a derived class, it will be virtual if the base destructor is virtual but you can say it is virtual anyway, say so and declare virtual ~Derived() = default; but only if the compiler generated destructor does the right thing.
And the same for virtual inheritance.
The key thing is will the compiler-generated destructor do the right thing (part of rule of 0). If so, let the compiler generate it for you.
Jason's advice generalises to default constructors, copy constructors, assignment operators, move constructors and , move assignment operators. If the compiler is not going genrate the wrong thing, then let it go ahead.
I have plenty of empty destructors! Lol
As others have already commented, I also use empty destructors for abstract classes. What's the best way to handle this?
GCC seems to be optimizing for speed, clang looks like optimizing for size. Have you tried to use -Os for GCC?
The same options were used for each one.
Just what I think of when someone says "code bloat". Some generated compiler assembly from destructors /s
With -O2 optimization
struct S {
std::string s;
};
and
struct S {
std::string s;
~S() {}
};
do exactly the same thing and lead to exactly the same object code. It's possible that with no optimization or a bad compiler the one with the explicit destructor takes more microprocessor instructions, but any decent compiler can figure out the two versions are identical.
Inside a vector? Inside of vector, inside a class, inside a vector? You see, at some point the optimizer will give up and this is why it is important to have quality code in the first place and enjoy the optimizer only as a bonus, not count on it to write you the desired program. And as aside, there are environments that work in debug 99.9% of the time.
What about dangling pointer? (In your pointer example)
en.m.wikipedia.org/wiki/Dangling_pointer
What if I always define everything? I define all my copy and move operators properly (=default, =delete on copy sometimes) and a default dtor. All in the header. Would that be any different?
It's not a good idea. It means your classes will never be trivial even if they could otherwise be, which disables a number of optimizations in the compiler and in the standard library. And there are lots of pitfalls you can make (for example, are you correctly labeling all of your move constructors as noexcept whenever possible? if not, then vector operations will be much more expensive than they otherwise could be because the vector must use the copy constructor when resizing).
This is such a good clickbait title
What if I did `~S() = default;`?
Am I defining a destructor when I do ~ClassName() = default; ?
You are not defining a destructor, but you've signaled to the compiler that you want to do something special with the other special member functions and it does affect code generation.
play with this example:
compiler-explorer.com/z/887q9xd1T
Hello and thank you for your job!
At the end of this episode you told about PIMPL idiom and about the rule of 5. I've been playing around with the compiler with this idiome based on unique_pointer and as a result did not watch any overhead in generated code. So why should we write more code than required making it harder to read?
Thank you!
Here is my code example: godbolt.org/z/BxU4Jw
You can play it by comment out the `#define optimize`. Also you can watch how it works for both: creating an object and emplacing it into the vector (Need to uncomment also move cons).
why make an useles destructor in the first place tho
Isn't this just the case of transforming a POD struct into a non-POD?
Yes
Help! What editor is that? How to see the assembly code on your c++ code. Im a newbie i want to learn assembly. Thanks.
check this website: godbolt.org/
wow FML
I think I might have written like 3 destructors in my life, not feeling too concerned here
I was expecting "Don't use std::embed on a selfie."
why no downvotes, how can I know potential bad advice
Reducing a assembly bloat, this isn't going to help the source code bloat
Well, when you're removing ASM bloat by removing source code, it kinda is...
Code bloat as a term in general refers to code size in the binary - the assembly bloat.
Noexcept destructor? .... Hmm... If the destructor will throw exceptions, the program will be ill-formed ...
You are allowed to throw an exception from a destructor if you really want to. It's not ill-formed.
stackoverflow.com/questions/41174167/c-exception-in-destructor
@@cppweekly OK, perhaps "ill-formed" is not correct term here.
But what about
1) finishing the destructor (freeing object's memory)?
2) execution of the base class destructor?
AFAIUI (may be incorrectly) problems are so hard that program termination is really the only correct solution. :-)
@@cppweekly OK, perhaps "ill-formed" is not correct term here.
But what about
1) finishing the destructor (freeing object's memory)?
2) execution of the base class destructor?
AFAIUI (may be incorrectly) problems are so hard that program termination is really the only correct solution. :-)
wtf yould i have a destructor that does nothing :))) it is just a waste of space and looks ugly
Desperation tactic for catching use-after-free and memory corruption bugs.
these useless dtors sound like something a future compiler will optimize away
I usually set default constructor, copy, move, copy assign, move assign, and destructor all to default. Is that also bad?
That's probably technically OK, but why? You're telling the compiler to do what it already wanted to do already if you hadn't told it to do anything.
@@cppweekly Mostly, there is a code template (not C++ template) that I can summon in with the defaulted special member functions. That way, if I need them, I don't have to type out their function prototypes, only the body, if I don't, they stay as defaulted, it is for convenience.