The avrgcc compiler supports C ++, but its delivery does not include either the standard library or the ABI implementation: utility functions, the calls of which are inserted by the compiler itself. As a result, people try to realize the parts that they need, on their own and often do it not very well. For example, it is often proposed to shoot yourself a leg by defining an empty function __cxa_pure_virtual (void) {} ​​or put a rake on yourself by writing caps for __cxa_guard_acquire, __cxa_guard_release and __cxa_guard_abort. In this article, I propose to figure out what is missing for happiness, where to get it or how to write it.
I know that many people think that C ++ is not needed on the microcontroller. I ask them to read the last section of the article before writing comments.
Features for arduino owners
Arduino provides limited support for C ++. But, as far as I understand, the developers of arduino do not like C ++, therefore, at the request of the workers, the first available crutch was inserted into the appropriate module. It turned out to be a crutch described on
avrfreaks , and without the corrections indicated in the comments to the topic. So first you have to get rid of it. Delete files
- hardware / arduino / cores / arduino / new.h
- hardware / arduino / cores / arduino / new.cpp
Or use the
version where it is already done .
Pure virtual and remote methods
The mechanism of virtual methods, as a rule, is implemented through vtable. This is not regulated by the standard, but is used in all compilers. Even if you declare a method to be purely virtual, that is, not having an implementation, the vtable will still have space for a pointer to this method. This is necessary in order for the child classes to put a pointer to the corresponding implementation at the same offset. Instead of a pointer to the missing method, the compiler writes a pointer to the stub function __cxa_pure_virtual in the vtable. If someone manages to call a purely virtual function, then the control will switch to a stub and it will stop the program, instead of trying to execute a random piece of memory. Please note that this protection is almost free: the implementation of __cxa_pure_virtual, containing a single call, takes only 6 bytes of flash.
A reasonable question arises, how is it possible to call a purely virtual function if it is impossible to create an object of an abstract class? You cannot create it, but you can call it if you write strange code:
class B { public: B() { // C , vtable B // , // virt(), non_virtual(); } void non_virtual() { // , // , // B virt(); // pure virtual method called // terminate called without an active exception // (core dumped) } virtual void virt() = 0; }; class C : public B{ public: virtual void virt() {} }; int main(int argc, char** argv) { C c; return 0; }
To avoid such errors, you should try not to call the methods of the object until it is initialized. In other words, do not do the hard work in the constructor. And for the compiler to build your application, add the implementation of the following functions:
void __cxa_pure_virtual(void) { // We might want to write some diagnostics to uart in this case std::terminate(); } void __cxa_deleted_virtual(void) { // We might want to write some diagnostics to uart in this case std::terminate(); }
You will also need the std :: terminate implementation from the standard library. If you really need to save 2 bytes of RAM and 14 bytes of flash, you can also call abort () directly, but later I will explain why std :: terminate is preferable.
')
Static variables
You can declare a variable inside a function as static, which in fact makes it a global variable visible only inside the function.
int counter(int start) { static int cnt = start; return ++cnt; }
In C, this code will not be collected, since a static variable must be initialized with a constant in order to immediately place the initial value in the .data section. In C ++, this was allowed, but it was not specified what would happen if two threads tried to initialize the variable at the same time. Many compilers have chosen to add locks and ensure thread safety in this case, and in C ++ 11 this behavior has become part of the standard. Therefore, initialization is not a constant value of the static variable gcc will expand to the following code: gcc / cp / decl.c
static <type> guard; if (!guard.first_byte) { if (__cxa_guard_acquire (&guard)) { bool flag = false; try { // Do initialization. flag = true; __cxa_guard_release (&guard); // Register variable for destruction at end of program. } catch { if (!flag) __cxa_guard_abort (&guard); } } }
where guard is an integer type of sufficient size to store the flag and mutex. Viewing the gcc source showed that its optimization only bothered on the ARM architecture:
gcc / config / arm / arm.c
static tree arm_cxx_guard_type (void) { return TARGET_AAPCS_BASED ? integer_type_node : long_long_integer_type_node; }
In all other cases, the default type is used: long_long_integer_type_node. On avr, depending on the -mint8 option, it will be either 64 or 32 bits. 16. guard.first_byte, in which the flag is placed, is understood by the compiler as the byte with the lowest address: * (reinterpret_cast <char *> (g)). An exception is the ARM platform, where only one bit of the first byte is used.
How correct?
If you do not need thread-safe static variables, disable them with the -fno-threadsafe-statics option and the compiler, instead of complex locks, will provide a simple flag check. Implement __cxa_guard_ * in this case is not necessary. But if you provide them (as is done in arduino), then the implementation
should ensure correct operation in the case of simultaneous initialization of a variable from regular code and from an interrupt. In other words, __cxa_guard_acquire should block interrupts, and __cxa_guard_release and __cxa_guard_abort should return them to their previous state. In the case of using RTOS, I may be willing to sacrifice correctness in interrupts, leaving correctness for the two threads. The correct implementation should work like this:
namespace { // guard is an integer type big enough to hold flag and a mutex. // By default gcc uses long long int and avr ABI does not change it // So we have 32 or 64 bits available. Actually, we need 16. inline char& flag_part(__guard *g) { return *(reinterpret_cast<char*>(g)); } inline uint8_t& sreg_part(__guard *g) { return *(reinterpret_cast<uint8_t*>(g) + sizeof(char)); } } int __cxa_guard_acquire(__guard *g) { uint8_t oldSREG = SREG; cli(); // Initialization of static variable has to be done with blocked interrupts // because if this function is called from interrupt and sees that somebody // else is already doing initialization it MUST wait until initializations // is complete. That's impossible. // If you don't want this overhead compile with -fno-threadsafe-statics if (flag_part(g)) { SREG = oldSREG; return false; } else { sreg_part(g) = oldSREG; return true; } } void __cxa_guard_release (__guard *g) { flag_part(g) = 1; SREG = sreg_part(g); } void __cxa_guard_abort (__guard *g) { SREG = sreg_part(g); }
How much is
If you do not use static variables or assign constant values ​​to them, it's free. If you specify the -fno-threadsafe-statics flag, then pay 8 bytes of RAM for the flag, and 12 bytes of flash for each variable. If you are using thread-safe initialization, spend another 38 bytes of flash on each variable and another 44 on the entire program. In addition, interrupts will be blocked during the initialization of static variables. But you do not do difficult work in designers?
The choice is yours, but in any case, if the library provides the __cxa_guard_ * functions, they should be implemented correctly, and not be the gag that is offered everywhere. In general, I would recommend trying not to use static variables.
Where to get
abi.h and
abi.cpp
operator new and operator delete
When it comes to the operators new and delete, someone will surely say that there is very little memory in microcontrollers, so dynamic memory is an unaffordable luxury. These people do not know that new and delete are not only dynamic memory management. There is also placement new, which has the object in the buffer allocated by the programmer. Without it, you cannot write a ring buffer, which is loved by the firmware developers, through which message queues are implemented. Well, if you are so sure that dynamic memory is not needed, then why did you write implementations for malloc and free? So there are tasks where it was impossible to manage without them.
Types of operators new and delete
First, there is an operator new allocating memory for single objects and there is an operator new [] allocating memory for arrays. Technically, they are distinguished by the fact that new [] remembers the size of the array in order to cause a destructor for each element during deletion. Therefore, it is important to use the paired operator delete or operator delete [] when freeing memory.
Second, each of these statements, like any function in C ++, can be overloaded. And the standard defines three options:
void* operator new(std::size_t numBytes) throw(std::bad_alloc)
will allocate a block of memory of size numBytes. In case of an error, throws an exception std :: bad_alloc void* operator new(std::size_t numBytes, const std::nothrow_t& ) throw();
will allocate a block of memory of size numBytes. If an error occurs, returns nullptr inline void* operator new(std::size_t, void* ptr) throw() {return ptr; }
placement new, locates the object where they said. Used when selling containers
In arduino, for unknown reasons, only void * operator new (std :: size_t numBytes) throw is implemented (std :: bad_alloc), and in the event of an error it returns 0, which leads to unspecified behavior of the program, since nobody checks the returned value .
With operator delete, everything is a little trickier. There are void * operator delete (std :: size_t numBytes) and void * operator delete [] (std :: size_t numBytes). You can overload it for other parameters, but you can not cause these overloads, because the language does not have the appropriate syntax. There is only one case where the compiler will cause an overloaded version of the delete statement. Imagine that you are creating an object in dynamic memory, the new operator successfully allocated memory, the constructor began to fill it, and threw an exception. Your code has not yet received a pointer to an object, so that it cannot return the memory of the failed object to the system. Therefore, the compiler is forced to do this by calling delete. But what happens if the memory was “allocated” using placement new? In this case, you cannot call normal delete, therefore, if the constructor threw an exception, the compiler will call the overloaded version of delete with the same parameters that new was called with. So the standard library defines three versions of operator delete and three versions of operator delete [].
Processing bad_alloc
As mentioned above, the most frequently used version of new is required to throw an exception in case of an error. But gcc does not support exceptions to avr: they can neither be thrown nor caught. But if they cannot be caught, then there is not a single try section in the program, which means that if an exception were thrown, then std :: terminate would be called. Moreover, the C ++ standard allows in this case (see 15.5.1) not unwinding the stack. Therefore, new can call std :: terminate directly and it will conform to the standard.
Do not be alarmed that the standard library will take and complete the firmware! How often can you fix something if bad_alloc occurs? As a rule, nothing. Your firmware can not continue to work correctly and thank God that it will end at the time of the error. But if you know how to fix the situation, you can use the nothrow version of the new operator. Look at this as a safe malloc, which correctly behaves if you do not check the return value.
Where to get
In uClibc ++ there is a complete and correct implementation of new and delete. True, instead of std :: terminate, abort () is called there. Therefore, I made a
corrected version. At the same time, there are added initialization lists, std :: move and std :: forvard.
std :: terminate vs abort ()
According to the avr-libc
documentation , the abort () function blocks all interrupts, and then falls into an infinite loop. This is not what I would like. For two reasons. First, it leaves the device in a
dangerous condition . Imagine that the system controls the heating element and the program loops when it is turned on. In case of an error, I want to go to a safe state by setting all the outputs of the board to 0. Secondly, I already know that everything is bad and I don’t have to wait until the watchdog is triggered and the system is rebooted. This can be done immediately.
If the firmware is terminated by std :: terminate, I can install my own handler and perform all the necessary actions there. I cannot redefine abort: the mechanism provided for this in unix does not work on avr. Therefore, I would rather spend those 2 bytes of RAM and 14 bytes of flash, which is occupied by the implementation of std :: terminate.
Exceptions
Exceptions are that part of C ++ that you have to pay a lot for, not only in the code that uses them directly, but also in the code through which the exception can fly: the compiler is forced to register the destructor of each variable created on the stack. In addition, for exceptions, RTTI and a small backup memory buffer are needed, so that you have where to create std :: bad_alloc when memory runs out. In addition, this is the only part of C ++ for which it is problematic, although not impossible, to calculate the execution time. As far as I understand, anyone who understood enough to write the functions for supporting exceptions that were missing on the AVR lost the desire to do so. There are many more important things to do too. Therefore, there is no support for exceptions on AVR in gcc and, quite likely, there will not be.
STL
I have seen many reports that STL on a microcontroller is bad: it inflates the code and, by making complex things simple, incites to use them. At the same time, it is silent that in STL there are such primitives as
quick sorting , which is much faster and more compact qsort, or
binary search , safe versions of min and max. Do you really know the sacred way to write classical algorithms more effectively than other programmers? And then why not use a ready-tested algorithm that takes as much space as what you have to write. For those parts of STL that you do not use, you do not pay.
Where to get
Use
uClibc ++ . This library has one feature: std :: map and std :: set are implemented on top of the vector, so iterators are disabled when inserting and deleting. In addition, they have a different complexity. In the documentation, the author describes in detail why he did so.
What is C ++ for?
This is a topic for a separate article that I am ready to write if you are interested. In short, with
proper use, C ++ allows you to write solutions that are as effective as C solutions, but at the same time get more readable and more secure code through compiler checks. And the template mechanism allows you to write efficient implementations of generalized algorithms, which is problematic in C. I also got used to it. In any case, I highly ask you to refrain from discussing this topic now.