Static Library; Unused Symbols; Definitions in Header

179 Views Asked by At

I'm creating an HAL for an embedded system and part of that is re-creating printf functionality (via a class called Printer). Because it is an embedded system, code-space is critical and I would like to exclude floating-point support in printf by default, but allow the user of my HAL to include it on a project-by-project basis without having to recompile my library.

All of my classes have their method definitions inline in the header file.

printer.h looks something like....

class Printer {
    public:
        Printer (const PrintCapable *printCapable)
             : m_printCapable(printCapable) {}

        void put_char (const char c) { ... }

#ifdef ENABLE_PRINT_FLOAT
        void put_float (const float f) { ... }
#endif

        void printf (const char fmt[], ...) {
            // Stuffs...

#ifdef ENABLE_PRINT_FLOAT
            // Handle floating point support
#endif
        }

    private:
        const PrintCapable *m_printCapable;
}

// Make it very easy for the user of this library to print by defining an instance for them
extern Printer out;

Now, it is my understanding that this should work great.

printer.cpp is nice and simple:

#include <printer.h>
#include <uart/simplexuart.h>

const SimplexUART _g_simplexUart;
const Printer     out(&_g_simplexUart);

Unnecessary code bloat: If I compile my library with and project without ENABLE_PRINT_FLOAT defined, then code size is 9,216 kB.

Necessary code bloat: If I compile both library and project with ENABLE_PRINT_FLOAT, code size is 9,348 kB.

Necessary code blo.... oh wait, it's not bloated: If I compile the project with and the library without ENABLE_PRINT_FLOAT, I would expect to see the same as above. But no... instead I have code size of 7,092 kB and a program that doesn't execute correctly.

Minimum Size: If I compile both are compiled without ENABLE_PRINT_FLOAT, then the code size is only 6,960 kB.

How can I achieve my goal of small code size, flexible classes, and easy-to-use?

Build system is CMake. Full project source is here.

Main file is nice and simple:

#include <printer.h>

void main () {
    int i = 0;

    while (1) {
        out.printf("Hello world! %u %05.2f\n", i, i / 10.0);
        ++i;
        delay(250); // 1/4 second delay
    }
}
1

There are 1 best solutions below

9
On BEST ANSWER

If you have different definition of inline functions in different translation units you have undefined behavior. Since your printf() definition changes with the setting of the ENABLE_PRINT_FLOAT macro you just see this effect.

Typically the compiler won't inline functions if it considers them too complicated. It would create out of line implementations and pick a random one when linking. Since the are all the same picking a random is OK ... oh wait, they are different and the program may be broken.

You could make floating point support a template parameter of your printf() function: the function would be called using

out.printf<false>("%d\n", i);
out.printf<true>("%f", f);

The implementation of printf() would delegate to suitable internal functions (to have the compiler merge definitions where they are identical) with the floating point support being disabled for the false case: it could do nothing, fail, or assert.

It may be simpler not do any conditional support in the first place and rather use a stream-like interface: since the formatting functions for the different types are separate, only those actually being used are picked up.

If it is an option for you library to use C++11 you could use variadic template to deal with the situation: the individual formatter would be implemented as separate functions which are dispatched to inside printf(): this way there is no printf() function which needs to handle all formatting. Instead, only the type formatters needed would be pulled in. The implementation could look something like this:

inline char const* format(char const* fmt, int value) {
    // find format specifier and format value accordingly
    // then adjust fmt to point right after the processed format specifier
    return fmt;
}
inline char const* format(char const* fmt, double value) {
    // like the other but different
}
// othe formatters

inline int printf(char const* fmt) { return 0; }
template <typename A, typename... T>
inline int printf(char const* fmt, A&& arg, T&& args) {
    fmt = format(fmt, std::forward<A>(arg));
    return 1 + printf(fmt, std::forward<T>(args));
)

Clearly, there are different approaches how common code between different formatter can be factored out. However, the overall idea should work. Ideally, the generic code would do as little work as possible to have the compiler merge all non-trivial code between the different uses. As a nice side-effect this implementation could make sure that the format specifiers are matching the objects being passed and either produce a suitable error or appropriately handle the format in some way.