On Apr 25, 2006, at 10:48 AM, dimax un wrote:

Never heard that C++ makes small code especially templates and especially in embedded systems.

Actually, templates are the best hope for small code using C++.

The secret is to have the right kind of templates, and to use the compiler to do as much work as possible at compile time, rather than making code on the embedded system do the work at runtime.

Templates have gotten a bad reputation for several reasons.

- some of the early implementations were less than optimal in their operation - the standard took a while to solidify, after some experience with the STL stuff - the Embedded C++ people shunned them for their own reasons, and then produced a number of documents blaming templates (and other features that weren't present in C++ as of 20 years ago) for their problems with C++. - template instantiation was badly done in some compilers, leading to duplication - if you aren't careful about how you write templates you can end up with code duplication where it's not needed (for instance, instantiations for signed and unsigned flavors of template parameters that are only used as indexes in arrays (and so would only be positive in typical usage).

But there's nothing inherent in templates themselves that should produce bigger code than equivalent C code.

Now, I'm not using the Standard C++ library as yet; I'm just using my own templates for the time being.

Where can template help C++?

Let's take polymorphism. There are two kinds of polymorphic behavior available in C++; one (the runtime flavor) has to do with using pointers to objects that have compatible interfaces (in C++, of course, compatibility is determined by inheritance).

The two kinds are orthogonal, of course; you often need to have both kinds.

Runtime polymorphism requires building a lookup table (the "vtable") in memory somewhere (typically in RAM) that holds pointers to the various virtual member functions; the object itself (also typically in RAM) then points to that table.

If you really need runtime polymorphism, that's an efficient way to do things.

But it's silly to pay that price when you know (or can compute) at compile time the actual class of the receiver (the object on which you're calling the member function). And this is a very common idiom with smaller processors: we know up front what we're using USART1 for, and what we're using USART2 for. Likewise in the cases where we have multiple units of similar hardware in our systems (CAN message objects, timers, ADC channels, etc.).

For instance, I've seen code on the net that uses C++ for hobby robotics education (using the AVR). It's cleanly written code that would have been legal C++ 20 years ago (as I recall; I don't remember precisely after all these years what the changes were between Cfront 1.1 and Cfront 1.2). No templates, namespaces, use of #define instead of const, etc.

You can get the code here:
http://www.seattlerobotics.org/WorkshopRobot/level1/Level2Files.zip

An example from this library (representing an input bit):

====
class IN
{
public:
        /* constructor for setting the register ('A', 'B', etc)
           and pin (0-7), and specifying whether to enable the pull-up
           resistor [pull-up is on by default if not specified] */
        IN(char reg, char pin, bool fPullup = true);
        
        /* returns true if pin is reading high, else false */
        bool IsHigh() const;
        
        /* returns true if pin is reading low, else false */
        inline bool IsLow() const { return !IsHigh(); };
        
        /* helpers for switches */
        inline bool IsOpen() const   { return IsHigh(); }
        inline bool IsClosed() const { return IsLow();  }
        
        /* checks for button press; if not pressed, returns false; else
           waits for release (including debouncing both press & release) */
        bool WasPressed() const;
        
private:
        unsigned char m_bit;
        volatile uint8_t *m_preg;
};

IN::IN(char reg, char pin, bool fPullup)
{
        volatile uint8_t *preg; // pointer to PORT register (for pull-up R)
        
        Assert(pin >= 0);
        Assert(pin <= 7);
        
        m_bit = 0x01 << pin;
        
        /* set direction for input; set m_preg, preg */
        switch (reg)
        {
        default:
                m_bit = 0;      // error-handling
                Assert(false);
                // fall through so m_preg gets set...
        case 'a':
        case 'A':
                DDRA &= ~m_bit;
                m_preg = &PINA;
                preg = &PORTA;
                break;
        case 'b':
        case 'B':
                DDRB &= ~m_bit;
                m_preg = &PINB;
                preg = &PORTB;
                break;
        case 'c':
        case 'C':
                DDRC &= ~m_bit;
                m_preg = &PINC;
                preg = &PORTC;
                break;
        case 'd':
        case 'D':
                DDRD &= ~m_bit;
                m_preg = &PIND;
                preg = &PORTD;
                break;
        }
        
        if (fPullup)
                *preg |= m_bit;         // enable pull-up resistor
        else
                *preg &= ~m_bit;    // disable pull-up resistor
}

/* returns true if pin is reading high, else false */
bool IN::IsHigh() const
{
        return *m_preg & m_bit;
}

/* checks for button press; if not pressed, returns false; else
   waits for release (including debouncing both press & release) */
bool IN::WasPressed() const
{
        extern TIMER timer;

        if (IsOpen())
                return false;

        timer.WaitMs(msDebounce);
        while (IsClosed())
                ;
        timer.WaitMs(msDebounce);
        return true;
}

====


So this code takes three extra bytes of RAM (in addition to the three hardware registers DDRx, PORTx, and PINx) per input port. And every access requires looking at all of those bytes (dereferencing a pointer, masking with a value from RAM).

But why? There's no way to change m_preg or m_bit once you've constructed this object, so there's no real point in keeping those things in RAM. Especially when you look at how these are used: all of the IN objects are globals, and there's no passing of pointers to these things during runtime (after construction; some global constructors get passed IN or OUT object pointers). And there are no virtual functions here (though of course templates work well with virtual functions) so we don't need vtables. And I'm pretty sure that, even with duplicated code snippets from template expansion, the total ROM consumption would be less, as well.

Likewise, it's trivial to do compile-time asserts using templates for template parameters (like 0 <= pin <= 7 above). No reason for a run- time check when those parameters are compile-time constants!

The attached .cpp file shows the two styles; the OLD_STYLE is the above (trimmed down to just the I/O part) and the templates are a quick demo of the same.

You can compile it both ways (depending on the definition of the OLD_STYLE macro) and compare the resultant code.

Note that:

* the OLD_STYLE includes functions that aren't called, because they were in the source file. This forces library writers to break up their source files to one function per file, more or less.

* using templates, only the functions that are called are instantiated.

* the template code is much smaller:

   text    data     bss     dec     hex filename
   1188       0      12    1200     4b0 test-old.elf (1985-style C++)
    320       0       4     324     144 test-new.elf (with templates)

* The compiler seems to reserve 1 byte of RAM for each object anyway if you have a constructor for the class, even if you don't use it for anything (see the 4 bytes in the .bss section for test-new above). However, without constructors, the templates for I/O registers don't take any RAM at all.

Try it for yourself!
--
Ned Konz
MetaMagix embedded consulting
[EMAIL PROTECTED]


[1] As I see it, Embedded C++ was a fear-motivated standard. Two major fears are apparent to me: - the fear (shared by the managers of development organizations in big companies) of what the average- and below-average programmers might do (and of the training budget required to keep them from doing it) - the fear by some of the compiler vendors that they might actually have to spend money updating their compilers before they were ready to do so

Attachment: test.cpp
Description: Binary data

_______________________________________________
AVR-GCC-list mailing list
AVR-GCC-list@nongnu.org
http://lists.nongnu.org/mailman/listinfo/avr-gcc-list

Reply via email to