Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Added missing virtual destructors#218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
andreagilardoni wants to merge6 commits intoarduino:master
base:master
Choose a base branch
Loading
fromandreagilardoni:virtual-destructors

Conversation

andreagilardoni
Copy link
Contributor

@andreagilardoniandreagilardoni commentedSep 27, 2023
edited
Loading

When we have classes that may be derived in c++ it is important to always put a virtual empty destructor, in particular on abstract classes, because this can lead to memory leaks.

@codecov-commenter
Copy link

codecov-commenter commentedSep 27, 2023
edited
Loading

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base(1cec094) 95.53% compared to head(c37cff5) 95.54%.

Additional details and impacted files
@@           Coverage Diff           @@##           master     #218   +/-   ##=======================================  Coverage   95.53%   95.54%           =======================================  Files          16       17    +1       Lines        1075     1077    +2     =======================================+ Hits         1027     1029    +2  Misses         48       48

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report?Share it here.

Copy link
Contributor

@aentingeraentinger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Not sure if the general assumption that not having a virtual destructor leads to memory holds water, but there should be a virtual destructor if there's at least one virtual function in the class (or in a base class).

gillesdegottex reacted with thumbs up emoji
Copy link
Member

@facchinmfacchinm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Need further investigation, both on functionality and code size increase (if any)

@JAndrassy
Copy link
Contributor

JAndrassy commentedOct 3, 2023
edited
Loading

It is enough to add virtual destructor to Print, because Stream, Client, Server, HardwareI2C and HardwareSerial are inherited from Print

withvirtual ~Print() {}, Blink with SAMD 1.8.13 takes 150 bytes more flash memory (11608 - 11448) and 4 bytes more SRAM.
WiFiNINA WiFiWebServer example takes 160 bytes more flash (20484 - 20308) and has no change in reported global SRAM usage.

@andreagilardoni
Copy link
ContributorAuthor

@JAndrassy I think that removing the virtual destructor from the classes derived from Print may lead to the same issue when referencing, for example, a derived class of Client with aClient* pointer, which is the reason why I opened this PR.

@JAndrassy
Copy link
Contributor

@andreagilardoni I think the existence of virtual destructor in the top class of the hierarchy is enough. it forces the runtime to look into the table of virtual functions and execute the right destructor (as it works for any other virtual function)

mverch67 reacted with thumbs up emoji

@andreagilardoni
Copy link
ContributorAuthor

@JAndrassy I tried that and you are right. Below you can find how I checked that, for future reference. I will then delete the virtual modifier from derived classes

#include<iostream>classA {public:virtual~A() { std::cout <<"destroyng A" << std::endl; }virtualvoidfoo() = 0;};classB:publicA {public:~B() { std::cout <<"destroyng B" << std::endl; }voidfoo() { std::cout <<"foo B" << std::endl; }};classC:publicB {public:~C() { std::cout <<"destroyng C" << std::endl; }voidfoo() { std::cout <<"foo C" << std::endl; }};intmain() {    std::cout <<"example 1" << std::endl;    A* a =newC();    a->foo();delete a;    std::cout <<"example 2" << std::endl;    B* b =newC();    b->foo();delete b;return0;}

@JAndrassy
Copy link
Contributor

@andreagilardoniPrint should have the virtual destructor, notStream

andreagilardoni reacted with thumbs up emoji

@alrvid
Copy link

The crucial point about virtual destructors is that if you delete an object of a derived class using a base class pointer, the base class must have a virtual destructor, or you will have undefined behavior. Which can have any implications really, depending on what the compiler chooses to do - not just, or even, memory leaks. Compilers can emit very strange machine code for undefined behavior, and the emitted code can vary a lot depending on many different factors that are hard to predict. So even thinking it's ok because you tested and nothing went wrong is a false assumption.

I think the rule about adding a virtual destructor if there's a virtual function arose from the Joint Strike Fighter project's coding standard (at least there's where I saw it first), but you can have undefined behavior even without virtual functions. It's not a matter of what is logical either, but a matter of the compiler writers being free to do whatever they like that makes their lives easier, when the compiler encounters undefined behavior.

So you can use one or both of the following rules:

  • Always add a virtual destructor to classes that can be inherited from, in case someone does something stupid in the future.

  • Always check that there's a virtual destructor present when you delete using a base class pointer (and preferably also comment that you checked it in your code, for people reviewing the code). Which is a more risky rule, since people might not know about it.

The relevant part of the standard is 5.3.5 point 3 for C++11, for example.

gillesdegottex and mverch67 reacted with thumbs up emojimanchoz reacted with eyes emoji

@gillesdegottex
Copy link

gillesdegottex commentedApr 13, 2024
edited
Loading

The crucial point about virtual destructors is that if you delete an object of a derived class using a base class pointer, the base class must have a virtual destructor, or you will have undefined behavior.

Not exactly. For example, in:

class A {  public:    ~A() {    }};class B : public A {  public:    [virtual] void fn() {    }};

We must add the virtual keyword above to get the undefined behaviour warning from gcc. It doesn't show without it.

Namely, there is absolutely no problem in having class inheritance without any virtual destructor (though the destructor of the inheriting class might not be called then if there is no virtual destructor on the base class and the delete function is called on a pointer to the base class, but that's another issue).

So, undefined behaviour is related to having any virtual function, not to inheritance.
Ex.Print does absolutely need a virtual destructor (currently has virtual functions).String does not (currently has no virtual functions).

@alrvid
Copy link

That doesn't contradict it being undefined behavior. There's no requirement in the standard to issue a diagnostic message for undefined behavior, neither is it forbidden to do so (see 1.3.24). Which means that a compilermay give you a warning about undefined behavior, or it may not. For sure, your example with the virtual keyword is an example of undefined behavior, and GCC is nice enough to warn you even though it's not required. But that doesn't mean that cases where it doesn't emit warnings aren't undefined behavior. The only way to determine if it's undefined behavior or not is to compare the code with the wording of the standard.

And 5.3.5 point 3 of the standard makes the case in this issue undefined behavior. Undefined behavior doesn't require your compiled code to break - it just allows the compiler writers to do whatever is convenient for them. They may do that in a way that makes the code work flawlessly on a particular compiler version. And then they might not decide to issue a warning for that code even though it contains undefined behavior. But you can never rely on a specific compiler version - even combined with a huge amount of testing of the generated code - to determine that there's no undefined behavior present. Even a future version of the same compiler might behave differently, and then, all that's required is a recompile to have the latent bug manifest itself. You might get lucky that they add a warning when they make the change, or they might not, because they're not required to by the standard.

gillesdegottex reacted with thumbs up emoji

@mverch67
Copy link

The missing destructor also leads to linker errors. I'm facing this exact issue when e.g. cross-compiling with newer gnu compiler versions gccarmnoneeabi code for nrf52 target on a raspberry pi.

In order to update catch2 the following changes were made:- CMakeLists.txt: import catch2 with FetchContent_Declare, this will  make it more flexible and easier to update- removed main for tests, since it is not mandatory and it doesn't add  any value of having it- renaming includes in all the files
error: moving ‘a’ of type ‘arduino::String’ to itself [-Werror=self-move]Which may happen if if GCC_VERSION is not defined, and instead __GNUC__is defined
since for catch2 '[' is not a valid character in the tag name
@andreagilardoniandreagilardoniforce-pushed thevirtual-destructors branch 2 times, most recently fromf617c42 to9dab7bbCompareFebruary 19, 2025 10:24
The purpose of this tests is to hihglight the need for a virtualdestructor through valgrind unit test execution
@andreagilardoni
Copy link
ContributorAuthor

andreagilardoni commentedFeb 19, 2025
edited
Loading

I want to bring this PR back to life. For this reason I wrote an additional unit test to highlight the need for this change.

I wanted to highlight this by means of valgrind, making it fail. This can be seenhere. In order for valgrind to trigger this error an update of catch2 is required (#246).

I am trying to understand how to provide a method to measure the flash footprint that this change might generate on cores, but it is not trivial to provide that kind of measurement and the code size increase depends on the amount of classes used that derive fromPrint andPrintable and this number is large and depends on the application. As far as now a simple hello world program increases flash usage by ~100bytes. This may be a problem on AVR platforms, we can consider to avoid putting it on them with some preprocessor black magic. Are there any other platforms where an increase in codesize of 1KB could be a source of troubles?

On platforms that interface with the network this can become an important issue, since Client interface would lack the virtual destructor, and in those cases we have plenty of flash and ram to use.

mverch67 and SRGDamia1 reacted with thumbs up emoji

@per1234
Copy link
Collaborator

Are there any other platforms where an increase in codesize of 1KB could be a source of troubles?

It might also be a concern for the "megaavr" architecture (e.g.,Arduino megaAVR Boards). The ATmega4809 of the Nano Every and UNO WiFi Rev2 has 48 kB of flash.

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@gillesdegottexgillesdegottexgillesdegottex left review comments

@aentingeraentingeraentinger requested changes

@facchinmfacchinmfacchinm requested changes

@matthijskooijmanmatthijskooijmanmatthijskooijman approved these changes

Assignees
No one assigned
Labels
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

-Wdelete-non-virtual-dtor compiler warnings
10 participants
@andreagilardoni@codecov-commenter@JAndrassy@alrvid@gillesdegottex@mverch67@per1234@matthijskooijman@aentinger@facchinm

[8]ページ先頭

©2009-2025 Movatter.jp