先说g++编译器,这部分使用MSVC编译会出错,后续会有修改方案
运用了多项c++新语法(c++11\14\17)折叠表达式、正则表达式分割字符串、变长参数模板等。
打印格式为:“文件名、行数、描述、变量名1:变量值1 变量名2:变量值2 ……”
,使用简单,仅需传递变量名即可。
首先看使用部分,后续总共有3个方案实现同样的效果,使用方法不变:
可以看出使用很简单,无需像printf一样需对每个变量考虑变量类型
int main()
{
int www = 5;
double sdf = 3.2;
string ABC = "aasdas";
TEST_LOG("just test", www, sdf, ABC);
TEST_LOG("just test2222");
}
期望打印结果为:
file: xx.cpp line: xx just test (www : 5)(sdf : 3.2)(ABC : aasdas)
file: xx.cpp line: xx just test2222
话不多说,直接上代码,解释见注释。
#include <iostream>
#include <vector>
#include <iterator>
#include <regex>
using namespace std;
template<typename T>
// 该类主要是为了在打印变长参数时,自定义格式,比如给每个变长参数添加空格等,此处是添加括号和变量名
class modifyArgLog
{
public:
modifyArgLog(T const& r, vector<string> &nameList) : ref(r)
{
name = nameList[0];
nameList.erase(nameList.begin());
}
friend ostream& operator<< (ostream& os, modifyArgLog<T> s) {
return os << "(" << s.name << " : " << s.ref << ')'; // 输出传递进来的参数和括号、变量名
}
private:
T const& ref; //指向从构造函数中传递进来的参数
string name;
};
template<typename ...Args>
void testLog(const char* file, int lineno, string description, string name, Args && ...args)
{
cout << "file: " << file << " line: " << lineno << " " << description << " ";
// 运用正则表达式将传入的变长参数名解析分割到vector中
regex re { "\\s{0,},\\s{0,}" };
vector<string> nameList = vector<string> {
sregex_token_iterator(name.begin(), name.end(), re, -1), sregex_token_iterator() };
// 运用折叠表达式将变长参数输出
(cout << ... << modifyArgLog(args, nameList)) << endl;
}
// #args中的#代表取出传入的变长参数的变量名
// ##args:如果可变参数被忽略或为空,“##”操作将去除掉它前面的那个逗号,避免编译错误
#define TEST_LOG(description, args...) testLog(__FILE__, __LINE__, description, #args, ##args)
结果:
[Running] cd "c:\Code\C++\Test\test\" && g++ test.cpp -o test -std=c++17 && "c:\Code\C++\Test\test\"test
file: test.cpp line: 44 just test (www : 5)(sdf : 3.2)(ABC : aasdas)
file: test.cpp line: 45 just test2222
[Done] exited with code=0 in 4.937 seconds
以上代码在VS2017无法通过编译,因为MSVC对可变宏参数的处理和GCC不同,为了使代码具备通用性,可同时在linux和windows平台下使用,因此改进代码如下。
使用方法不变,也就是不改变main函数,核心代码为:(变动部分见注释)
template<typename T>
class modifyArgLog
{
public:
modifyArgLog(T const& r, vector<string> &nameList) : ref(r)
{
name = nameList[0];
nameList.erase(nameList.begin());
}
friend ostream& operator<< (ostream& os, modifyArgLog<T> s) {
if (s.name == "") { //此处用空白字符来区分description,保证第一个字符串与变量名打印方式不同
return os << s.ref << " ";
}
return os << "(" << s.name << " : " << s.ref << ')';
}
private:
T const& ref;
string name;
};
template<typename ...Args>
void testLog(const char* file, int lineno, string name, Args && ...args)
{
cout << "file: " << file << " line: " << lineno << " ";
regex re{ "\\s{0,},\\s{0,}" };
vector<string> nameList = vector<string>{
sregex_token_iterator(name.begin(), name.end(), re, -1), sregex_token_iterator() };
// 宏中去掉公共参数description后,保证宏一定会有至少一个参数,这个参数即原来的description
// description之前是在第一行cout中直接打印,现在被放到了args中
// 为了区分,将传入modifyArgLog的nameList第一个值修改成空字符,就能在类中实现不同打印
if (!nameList.empty()) {
nameList[0].clear();
}
(cout << ... << modifyArgLog(args, nameList)) << endl;
}
// GCC中可以通过##__VA_ARGS__去掉逗号,MSVC不支持,所以将description去掉放入可变参数中,保证宏至少有一个参数
//(据说VS2019以上可以通过设置标准预处理器使得支持,此处未实践)
#define TEST_LOG(...) testLog(__FILE__, __LINE__, #__VA_ARGS__, __VA_ARGS__)
VS2017中的运行结果:
VSCODE中运行结果(使用g++)
[Running] cd "c:\Code\C++\Test\test\" && g++ main.cpp -o main -std=c++17 && "c:\Code\C++\Test\test Main 2\"main
file: main.cpp line: 50 just test (www : 5)(sdf : 3.2)(ABC : aasdas)
file: main.cpp line: 51 just test2222
[Done] exited with code=0 in 4.636 seconds
可以实现g++、msvc中一份代码相同结果。
进阶,以上代码还是不是最优的,会使生成的程序体积过大,因为生成模板实例过多,重复代码更多,同时代码不够简洁,最优代码如下,使用c++17 折叠表达式(逗号的妙用)以及stringstream将参数转为字符串列表,和参数名一样处理
同样的,使用方法不变,也就是不改变main函数,核心代码为:(变动部分见注释)
void testLog(const char* file, int lineno, const string &nameStr, const string &argStr)
{
regex re { "\\s{0,},\\s{0,}" };
vector<string> nameList = vector<string> {
sregex_token_iterator(nameStr.begin(), nameStr.end(), re, -1), sregex_token_iterator()
};
vector<string> argList = vector<string> {
sregex_token_iterator(argStr.begin(), argStr.end(), re, -1), sregex_token_iterator()
};
cout << "file: " << file << " line: " << lineno << " " << argList[0] << " ";
for (size_t i = 1; i < nameList.size(); i++) {
// 已经获取到参数名和参数值的vector列表,可以任意diy输出格式了,此处使用(name : arg)
cout << "(" << nameList[i] << " : " << argList[i] << ")";
}
cout << endl;
}
// 将参数输入到字符流中,用“,”隔开,然后再输出出来,参数名一样处理
template<typename ...Args>
string getArgs(Args && ...args)
{
stringstream sstream;
// 这里使用折叠表达式,括号部分(stream << args << ", ")实际可以理解成一个函数,args为参数
// “,...”即剩下的所有参数均执行括号部分args所执行的函数
// 即所有参数以字符串形式输入到流中,并以逗号隔开,以便后续分割处理
((sstream << args << ", "), ...);
return sstream.str();
}
#define TEST_LOG(...) testLog(__FILE__, __LINE__, #__VA_ARGS__, getArgs(__VA_ARGS__));
执行结果如下:
file: main.cpp line: 38 just test (www : 5)(sdf : 3.2)(ABC : aasdas)
file: main.cpp line: 39 just test2222
[Done] exited with code=0 in 4.609 seconds
项目中发现以下代码在某种极小概率下会引发regex库函数异常,于是自己写了个split函数。
regex re { "\\s{0,},\\s{0,}" };
vector<string> nameList = vector<string> {
sregex_token_iterator(nameStr.begin(), nameStr.end(), re, -1), sregex_token_iterator()
};
vector<string> argList = vector<string> {
sregex_token_iterator(argStr.begin(), argStr.end(), re, -1), sregex_token_iterator()
};
void splitString(const string &pattern, string srcStr, vector<string> &result)
{
int idx = srcStr.find(pattern);
while (idx != -1) {
string unitStr = srcStr.substr(0, idx);
result.push_back(unitStr);
srcStr = srcStr.substr(idx + pattern.size());
idx = srcStr.find(pattern);
}
if (!srcStr.empty()) {
result.push_back(srcStr);
}
}