一、泛型编程的调试
如果说泛型编程(模板编程和元编程)开发的难度可以理解的话,泛型编程的编译有点让不少开发者不能理解了。只要出一点点小的问题,那错误如山如海一样的扑来。让开发者一看到那些错误恨不得把屏幕延展到天上去才能看得清。那么能不能在泛型编程的错误中,增加一些提示或者如普通程序一样打印一些日志呢?肯定可以啊,而且方法还有不少。
二、泛型编程的常见错误类型及错误输出说明
在泛型编程中,常见的错误类型包括:
- 类型不匹配
即在模板编译过程中,类型的数据类型替换或推导有问题,导致的异常。 - 约束检查失败
也就是SFINAE的错误,不满足模板编程的中限定条件或约束机制。 - 依赖名称解析错误
即使用了错误的名称依赖,如下:
template<typename T>
class Demo {
public:
using vType = T;
void check() {
vType v; //OK
T::inType i; //ERROR:必须使用typename关键字
typename T::inType iOK; // OK
}
};
常见的错误往往有各种不同的形式表现出来,特别是在复杂的嵌套应用中,编译错误可能几屏都看不完,这就需要开发者耐心的去寻找根源和重要的问题产生点。
泛型编程不能像普通编程一样使用标准的输入输出(如std::cout)来进行,而泛型编程的编译问题是在编译阶段,无法使用运行时的输入输出机制。但这不代表没有别的办法,常见的主要是利用编译器的错误消息(编译器可以按它的编译报错误消息,那么也可以按开发者定义的错误消息来报)或者使用警告错误之类的消息来打印出相关的值或问题等。
三、具体形式
既然在编译期也可以有类似标准输出的情况,那么具体的形式有哪些呢?基础的方法主要有:
- 静态断言
static_assert,C++17支持在静态断言中进行字符串的拼接,C++20以后可以允许constexpr动态生成字符串(但需要看编译器的支持) - #warning 或 #pragma message(非标准)
用来在编译阶段提供一些警告信息和预定义消息。它一般只能用于预处理阶段,无法对模板参数或 constexpr值进行处理,在宏级别的调试还是有一定作用的 - SFINAE方式
可以利用SFINAE技术来实现错误的输出,比如采用std::enable_if和static_assert一起来实现。在C++20后,随着概念的引入以及consteval、进一步扩展的constexpr可以让这一方法变得更清晰简洁。 - 自定义错误输出类型
这种就比较看开发者个人的能力选择了,自定义一个错误输出模板,然后在实际的编程开发中可以引入并在适当的时机将其触发,并输出相关的信息即可。比如可以使用偏特化等技巧引入相关的错误输出。 - 宏定义输出
宏几乎是个创可贴,哪里都可以贴上来用几下,但用起来着实要好好考虑,一般不太建议。
四、例程和说明
下面给出一个具体例程:
#include <iostream>
#include <sstream>
#include <type_traits>
#include <vector>
#define CONCAT(x, y) x##y
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
// 1: static_assert
template <int N> struct check_and_output {
static_assert(N == 10, "N must be 10" TOSTRING(szieof(N) "bytes"));
static_assert(N == 10, "N must be 10");
};
void testCheckOutput() { check_and_output<10> c; }
// 2: #warning 或 #pragma message
void testProgramMessage() {
#warning "info:call testProgramMessage"
#pragma message("msg line: " TOSTRING(__LINE__)) //注意__LINE__这个宏
}
// 3:SFINAE
template <typename T, typename = std::enable_if_t<std::is_integral<T>::value>> void checkFunc1() { std::cout << "integral type" << std::endl; }
template <typename T, typename = std::enable_if_t<!std::is_integral<T>::value>> void checkFunc2() {
static_assert(std::is_integral<T>::value, "not integral type");
}
void testSFINAE() {
checkFunc1<int>(); // ok
checkFunc2<float>(); // err:not integral type
}
// 4: customize
//4.1
template <auto N> struct print_msg { static_assert(N != N, "output print message!"); };
//4.2 Equivalent to the method mentioned above
template <auto N> struct debug_msg { using type = typename std::conditional_t<true, void, std::integral_constant<decltype(N), N>>::no_type; };
// 4.3 better
// value
template <auto X> struct print_value_msg;
// type
template <typename T> struct print_type_msg;
void testCustomMsg() {
constexpr int msgID = 100;
debug_msg<msgID> d1;
print_msg<msgID> p1;
print_value_msg<msgID>{};
print_type_msg<std::vector<int>>{};
}
// 5: macro
#define PRINT_MSG(x) \
do { \
struct debug_info { \
static constexpr auto value = (x); \
using type = typename std::integral_constant<decltype(value), value>::type; \
}; \
using cause_err = typename debug_info::no_member; \
} while (0)
void testMacro() { PRINT_MSG("This is a debug message"); }
//6: C++20中的概念
template <typename T>
concept PrintInfo = requires(std::stringstream &ss, T x) {
{ ss << x } -> std::same_as<std::stringstream &>;
};
template <typename T> void check_type(T &&) { static_assert(PrintInfo<T>, " printinfo check type at runtime!"); }
int main() {
int a[] = {10};
TTT t;
check_type(t);
testCheckOutput();
testProgramMessage();
testSFINAE();
testCustomMsg();
testMacro();
std::cout << "demo end!" << std::endl;
return 0;
}
在上述的代码中,第1种和第4.1种方法看上去有些类似,其实是使用的不同的情况,第1种是对数据的检查而4.1是直接在这个位置打印。本质是没有区别的,只是4.1使用了一种特殊的情况始终输出消息而已。可以通过打印N的值及相关的消息来确定位置和内容来辅助定位错误。
而4.2中的std::conditional_t<true,A,B> 总是选择第一个类型 A即void,然后再去访问void::no_type。而void并没有这个成员,所以编译错误,同时在错误信息中显示N的值。4.3相对更容易理解,直接没有实例化则自动输出编译错和相关的值。
第5种宏定义实现其实和4.2中有些类似,先创建一个结构体debug_info,然后获取变量x值到一个静态常量表达式中value中,可以通过错误来提示当前的类型和要查看的值。不过在这个实例中,在测试的环境中只报错误信息,却无法得到相关的值。大家可自行验证是不是这种情况。
五、总结
泛型编程中的编译错误处理最麻烦的是面对大片的编译错误导致的开发者的无所适从。不过一般来说,重点的错误提示都在开始和结束部分。当中大多是其它引用的相关约束检查导致的错误信息。但最好的方法还是通过开发者能够自定义消息来定位相关错误更为容易操作。
其实,泛型编程还有其它方法可以实现类似的消息输出,大家可以不断的在实践中总结和实现自己的相关辅助类,慢慢积累,从而在泛型编程中能够更方便快捷的定位和解决编译错误。

2089






