0

我正在为嵌入式系统创建一个 HAL,其中一部分是重新创建printf功能(通过一个名为 的类Printer)。因为它是一个嵌入式系统,所以代码空间很重要,我想在默认情况下排除浮点支持printf,但允许我的 HAL 的用户在逐个项目的基础上包含它,而无需重新编译我的库.

我所有的类都在头文件中内联了它们的方法定义。

printer.h看起来像......

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;

现在,我的理解是这应该很好用。

printer.cpp很好很简单:

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

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

不必要的代码膨胀: 如果我在没有定义的情况下编译我的库和项目ENABLE_PRINT_FLOAT,那么代码大小为 9,216 kB。

必要的代码膨胀: 如果我用 编译库和项目ENABLE_PRINT_FLOAT,代码大小为 9,348 kB。

必要的代码 blo.... 哦,等等,它并不臃肿:如果我用 编译项目而没有 编译库ENABLE_PRINT_FLOAT,我希望看到与上面相同的内容。但是不...相反,我的代码大小为 7,092 kB,并且程序无法正确执行。

最小大小: 如果我编译都没有编译ENABLE_PRINT_FLOAT,那么代码大小只有 6,960 kB。

如何实现代码小、类灵活和易于使用的目标?

构建系统是 CMake。完整的项目源代码在这里

主文件很好很简单:

#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
    }
}
4

1 回答 1

2

如果您inline在不同的翻译单元中对函数有不同的定义,那么您就有未定义的行为。由于您的printf()定义随着ENABLE_PRINT_FLOAT宏的设置而变化,您只会看到这种效果。

如果编译器认为函数太复杂,通常不会内联函数。它会创建不符合要求的实现并在链接时选择一个随机的实现。因为都是一样的随机选择是可以的......哦等等,它们是不同的,程序可能会被破坏。

可以使浮点支持您的printf()函数的模板参数:该函数将使用

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

的实现printf()将委托给合适的内部函数(让编译器在它们相同的地方合并定义),并针对这种情况禁用浮点支持false:它什么都不做、失败或断言。

首先不做任何条件支持可能更简单,而是使用类似流的接口:因为不同类型的格式化函数是分开的,所以只有那些实际使用的函数才会被拾取。

如果您的库可以选择使用 C++11,您可以使用可变参数模板来处理这种情况:单独的格式化程序将被实现为分派到内部的单独函数printf():这样就没有printf()需要处理的函数所有格式。相反,只会引入所需的类型格式化程序。实现可能如下所示:

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));
)

显然,有不同的方法可以分解不同格式化程序之间的通用代码。但是,总体思路应该可行。理想情况下,通用代码会尽可能少地让编译器合并不同用途之间的所有重要代码。作为一个很好的副作用,此实现可以确保格式说明符与正在传递的对象匹配,并产生适当的错误或以某种方式适当地处理格式。

于 2014-12-01T04:19:27.883 回答