我正在为嵌入式系统创建一个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。
必要的代码膨胀....哦,等等,它并不臃肿:如果我在没有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.2fn", i, i / 10.0);
++i;
delay(250); // 1/4 second delay
}
}
如果你在不同的翻译单元中对inline
函数有不同的定义,你就有未定义的行为。由于您的printf()
定义会随着ENABLE_PRINT_FLOAT
宏的设置而变化,因此您只会看到此效果。
通常,如果编译器认为函数太复杂,它不会内联函数。它将创建行外实现并在链接时选择一个随机实现。由于都是一样的,随机选择是可以的......哦,等等,它们是不同的,程序可能会被破坏。
您可以将浮点支持作为printf()
函数的模板参数:该函数将使用
out.printf<false>("%dn", 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));
)
显然,有不同的方法可以分解不同格式化程序之间的公共代码。但是,整体想法应该有效。理想情况下,泛型代码将尽可能少地工作,让编译器在不同用途之间合并所有非平凡的代码。作为一个很好的副作用,此实现可以确保格式说明符与正在传递的对象匹配,并产生适当的错误或以某种方式适当地处理格式。