您现在的位置是:主页 > news > 外贸网站 栏目/制作一个网站需要多少费用
外贸网站 栏目/制作一个网站需要多少费用
admin2025/5/6 0:47:34【news】
简介外贸网站 栏目,制作一个网站需要多少费用,网站制作时滚动页怎么做,做自己卖东西的网站文章目录RTC_DCHECK使用示例工程示例1示例2示例3RTC_DCHECK源码分析CheckArgType枚举类Val类MakeVal函数AppendFormat函数ParseArg函数FatalLog函数LogStreamerLogStreamer<>LogStreamerWebRTC中没有使用标准库中的断言,而是自己封装了一套断言宏,…
文章目录
- RTC_DCHECK使用示例
- 工程
- 示例1
- 示例2
- 示例3
- RTC_DCHECK源码分析
- CheckArgType枚举类
- Val类
- MakeVal函数
- AppendFormat函数
- ParseArg函数
- FatalLog函数
- LogStreamer
- LogStreamer<>
- LogStreamer
WebRTC中没有使用标准库中的断言,而是自己封装了一套断言宏,功能更加丰富,在断言失败时可以提供更多的失败信息。
RTC_DCHECK使用示例
工程
示例工程:https://pan.baidu.com/s/1rbI2hwXpMA-Pb-i-zCdVWA
提取码:cenz
示例1
#include "checks.h"int main()
{RTC_DCHECK(100 != 100);return 0;
}
当RTC_DCHECK()的参数为false时,断言失败,结束进程。断言失败打印的信息有,断言失败的文件名
及行号
,系统全局变量保存的错误码,断言失败的语句。
示例2
#include "checks.h"int main()
{RTC_DCHECK_EQ(100, 200) << "100 is not equal to 200";return 0;
}
WebRTC提供了丰富的断言宏,当断言失败时,可以打印指定的日志信息。除了常用的宏函数RTC_DCHECK,还提供了其他便于判断大小的宏函数,如下:
RTC_DCHECK_EQ:判断两个参数是否相等
RTC_DCHECK_NE:判断两个参数是否不相等
RTC_DCHECK_LE:判断第一个参数是否小于等于第二个参数
RTC_DCHECK_LT:判断第一个参数是否小于第二个参数
RTC_DCHECK_GE:判断第一个参数是否大于等于第二个参数
RTC_DCHECK_GT:判断第一个参数是否大于第二个参数
示例3
#include "checks.h"int main()
{RTC_DCHECK(1 != 1) << "hello world " << 100 << " " << 3.14;return 0;
}
上面那些宏函数,在断言失败时可以打印后面的错误日志。
RTC_DCHECK源码分析
先分析一些前置的代码,为后面分析核心代码作铺垫。
CheckArgType枚举类
enum class CheckArgType : int8_t
{kEnd = 0,kInt,kLong,kLongLong,kUInt,kULong,kULongLong,kDouble,kLongDouble,kCharP,kStdString,kStringView,kVoidP,kCheckOp,
};
CheckArgType
是一个枚举类,它的成员用于标识数据的类型,例如CheckArgType::kInt用于标识int类型。这些标识类型组成数组,用于记录输出日志数据的类型,便于在打印日志时根据其类型按照对应的格式打印。
其中有两个成员需要单独说明一下:kEnd表示类型数组到结尾了;kCheckOp表示输出的日志中前两个数据时宏函数的参数,需要单独处理。除了RTC_DCHECK
,其他宏函数都是两个参数,在使用这些宏函数的时候,需要将其参数也打印出来,这两个参数的打印需要使用kCheckOp标识。
枚举类继承自int8_t
是为了节省空间。
Val类
template <CheckArgType N, typename T>
struct Val
{/*获取类型*/static constexpr CheckArgType Type() { return N; }/*获取值*/T GetVal() const { return val; }T val;
};
用于存放待打印的日志,这个类保存日志的数据及其数据的类型。通过枚举类保存的数据类型,用于指示如何打印数据。输出日志底层调用的是vsnprintf函数,一方面需要提供打印的数据,另一方面也需要提供打印的格式字符串,如%d、%f、%s等。例如,保存的数据是int类型的,则保存的类型是CheckArgType::kInt,在打印的时候根据类型CheckArgType::kInt,则会使用%d进行打印。
注意这个类的对象,在使用的时候只由MakeVal()函数产生。
MakeVal函数
MakeVal函数通过函数重载的方式,根据参数类型的不同,产生对应类型的Val对象。
inline Val<CheckArgType::kInt, int> MakeVal(int x)
{return {x}; /*struct:{x}是可以用于初始化Val的。*/
}
调用MakeVal函数,传入100,返回Val对象。Val对象中保存着100,且保存了其类型CheckArgType::kInt。
template <typename T,typename std::enable_if<std::is_enum<T>::value && !std::is_arithmetic<T>::value>::type* = nullptr>
inline decltype(MakeVal(std::declval<typename std::underlying_type<T>::type>()))
MakeVal(T x)
{return {static_cast<typename std::underlying_type<T>::type>(x)};
}
在C++中,枚举类型不能隐式转成整型,若需要转换需要使用static_cast将枚举类型强转成整型。
typename std::enable_if<std::is_enum<T>::value && !std::is_arithmetic<T>::value>::type* = nullptr用于判断T是枚举类型,不是算术类型,即只有枚举类型才能调用本函数。
枚举底层存储数据的是整型,默认是int类型,也可以使用其他类型,如使用char可以节省空间。typename std::underlying_type<T>::type是用于获取枚举T的底层数据类型的。
decltype(MakeVal(sftd::declval<typename std::underlying_type<T>::type>()))用于获取MakeVal函数的返回值类型。
static_cast<typename std::underlying_type<T>::type>(x)将枚举类型强转成整型。
AppendFormat函数
在字符串s
的后面,按照格式字符串fmt
中的格式,将变参添加到s
中。
void AppendFormat(std::string* s, const char* fmt, ...)
{/*定义变参列表*/va_list args, copy; /*获取变参...*/va_start(args, fmt);va_copy(copy, args);/*获取字符串的长度*/const int predicted_length = std::vsnprintf(nullptr, 0, fmt, copy);va_end(copy);if (predicted_length > 0) {/*获取字符串的长度*/const size_t size = s->size();/*扩容*/s->resize(size + predicted_length);// Pass "+ 1" to vsnprintf to include space for the '\0'./* * &(*s)[size] 得到string容器最后一个元素的下一个元素地址* 将新的字符串添加到原来字符串的后面*/std::vsnprintf(&((*s)[size]), predicted_length + 1, fmt, args);}va_end(args);
}
先通过vsnprintf函数获取待添加字符串的长度,将原字符串扩容,最后将新字符串添加到后面。
这段代码中涉及了C语言中变参的处理,在C语言中对变参的处理主要是通过标准库中提供的宏。这不是现在的重点,仅给出一个示例简单说明一下变参如何处理。
#include <stdarg.h>
#include <stdio.h>int sum(int num, ...)
{int total = 0;int arg, i;va_list ap; /*定义一个列表*/va_start(ap, num); /*获取形参中的变参 ...*/for (i = 0; i < num; i++){arg = va_arg(ap, int); /*在变参中读取一个数据*/total += arg;}va_end(ap); /*释放列表ap*/return total;
}int main()
{int m = sum(10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);printf("sum = %d\n", m);return 0;
}
输出的结果是:sum = 55
对变参的简单处理是有固定套路的,熟悉了套路,对变参的处理不太难。
ParseArg函数
bool ParseArg(va_list* args, const CheckArgType** fmt, std::string* s)
{/*格式数组到结尾了,不能再打印数据了,否则就非法了。*/if (**fmt == CheckArgType::kEnd)return false;switch (**fmt) {case CheckArgType::kInt:/*从参数列表中读取一个int类型数据,并添加到字符串s后面。*/AppendFormat(s, "%d", va_arg(*args, int)); /*va_arg(*arg,int)从参数列表arg中读取一个int大小数据。 */break;case CheckArgType::kLong:AppendFormat(s, "%ld", va_arg(*args, long));break;case CheckArgType::kLongLong:AppendFormat(s, "%lld", va_arg(*args, long long));break;case CheckArgType::kUInt:AppendFormat(s, "%u", va_arg(*args, unsigned));break;case CheckArgType::kULong:AppendFormat(s, "%lu", va_arg(*args, unsigned long));break;case CheckArgType::kULongLong:AppendFormat(s, "%llu", va_arg(*args, unsigned long long));break;case CheckArgType::kDouble:AppendFormat(s, "%g", va_arg(*args, double));break;case CheckArgType::kLongDouble:AppendFormat(s, "%Lg", va_arg(*args, long double));break;case CheckArgType::kCharP:/*在字符串s后添加一个字符*/s->append(va_arg(*args, const char*));break;case CheckArgType::kStdString:/*在字符串s后添加一个string字符串*/s->append(*va_arg(*args, const std::string*));break;case CheckArgType::kStringView: { /*在字符串s后添加string_view字符串*/const absl::string_view sv = *va_arg(*args, const absl::string_view*);s->append(sv.data(), sv.size()); /*根据字符串大小,直接添加。*/break;}case CheckArgType::kVoidP:/*在字符串s后添加指针*/AppendFormat(s, "%p", va_arg(*args, const void*));break;default:/*无法识别的类*/s->append("[Invalid CheckArgType]");return false;}(*fmt)++; /*将格式化字符串往后一个*/return true;
}
const CheckArgType** fmt用于标识数据如何打印,和printf函数中的%d、%f的作用有些相似。args提供输出的数据。
FatalLog函数
FatalLog函数调用fprintf函数向标准错误输出错误日志。
RTC_NORETURN void FatalLog(const char* file,int line,const char* message,const CheckArgType* fmt,...)
{va_list args;va_start(args, fmt); /*存放待输出的字符串*/std::string s;/*输出错误日志中固定的部分,并保存在s字符串中。*/AppendFormat(&s,"\n\n""#\n""# Fatal error in: %s, line %d\n""# last system error: %u\n""# Check failed: %s",file, line, LAST_SYSTEM_ERROR, message); if (*fmt == CheckArgType::kCheckOp) {fmt++;/*先解析变参中的前两个参数*/std::string s1, s2;if (ParseArg(&args, &fmt, &s1) && ParseArg(&args, &fmt, &s2))AppendFormat(&s, " (%s vs. %s)\n# ", s1.c_str(), s2.c_str());} else {s.append("\n# ");}/*循环的解析参数列表,并添加到字符串s中。*/while (ParseArg(&args, &fmt, &s)) ;va_end(args);/*将string转成const char*/const char* output = s.c_str();/*一般标准输出和标准错误都显示到console中,所以先刷stdout的缓冲。*/fflush(stdout);/*往标准错误中打印出错日志*/fprintf(stderr, "%s", output);/*刷标准错误缓冲*/fflush(stderr);/*因为严重的错误,需要将进程异常终止掉。*/abort();
}
除了RTC_DCHECK
,其他宏函数都是两个参数,在使用这些宏函数的时候,需要将其参数也打印出来。这两个参数通过CheckArgType::kCheckOp标识,若数组fmt中有此标识,需要将变参...
中的前两个参数取出,并按照指定格式打印。
while (ParseArg(&args, &fmt, &s)) ; 这条语句就是处理RTC_DCHECK
宏后,紧跟在<<运算符后的内容。将<<运算符后的数据,按照数据类型的格式输出到s
字符串中。具体细节在后面会详细展开。
LAST_SYSTEM_ERROR
宏展开后,在linux平台上中代表error全局变量,存放着最后的错误码。
RTC_NORETURN
在linux平台上展开后为__attribute__ ((__noreturn__)),这是给编译器使用的,表示FatalLog函数没有返回值。
LogStreamer
在示例3中,RTC_DCHECK(1 != 1) << "hello world " << 100 << " " << 3.14;语句中使用了operator <<运算符打印指定的数据,对于<< "hello world " << 100 << " " << 3.14;的处理,使用就是LogStreamer类,这个类将"hello world"、100、3.14、空格字符串
保存在类内部,以便下一步处理。
调用RTC_DCHECK宏的时候,使用<<运算符的数量是不固定的。在这里是通过变参模板LogStreamer将所有的数据递归的保存下来。核心思想是
:在从左到右处理<<运算符的时候,将数据递归的保存在LogStreamer中,处理完<<运算符后,再递归的将数据保存到Call函数或CallCheckOp函数的变参args中。
template <typename T, typename... Ts> /*变参模板类*/
class LogStreamer<T, Ts...>;template <> /*变参模板类的特化版本*/
class LogStreamer<>;
LogStreamer是一个变参模板类。
LogStreamer<>
先看一下特化版本。LogStreamer类重载了operator<<(),即<<运算符。
template <typename U,typename std::enable_if<std::is_arithmetic<U>::value || std::is_enum<U>::value>::type * = nullptr>
RTC_FORCE_INLINE LogStreamer<decltype(MakeVal(std::declval<U>()))> operator<<(U arg) const
{return LogStreamer<decltype(MakeVal(std::declval<U>()))>(MakeVal(arg), this);
}
特化版的operator<<()函数,在接收了数据以后,又调用非特别版的构造器函数,生成了LogStreamer对象,并将其返回。
typename std::enable_if<std::is_arithmetic<U>::value || std::is_enum<U>::value>::type * = nullptr用于判断U是否是算术类型或是枚举类型,当U是算术类型或枚举类型时,调用本函数。operator<<()函数还有一个重载版本,当U不是算术类型,也不是枚举类型时,调用另外一个重载版本。算术类型或枚举类型operator<<(U arg)传递形参用的是传值的方式,而非算术、枚举类型operator<<(const U& arg)传递参数采用的是传引用,这样可以避免拷贝,提高了效率。
RTC_FORCE_INLINE
宏展开后为__attribute__((__always_inline__)),给编译器使用的。在C++中inline是一个建议型关键字,使用inline的函数不一定会被编译器置为内联函数。而使用这个编译器属性,则强制编译器将本函数置为内联函数。
template <typename... Us>
RTC_NORETURN RTC_FORCE_INLINE static void Call(const char* file, const int line, const char* message, const Us&... args)
{static constexpr CheckArgType t[] = { Us::Type()..., CheckArgType::kEnd };FatalLog(file, line, message, t, args.GetVal()...);
}
变参args 中保存的是待输出的日志数据,在示例3
中,此时变参args保存的是"hello world"、100、3.14、空格字符串
,通过调试,可以看到变参args中保存的数据,如上图粉色部分。
args.GetVal()… 展开后为args0.GetVal(),args1.GetVal(),…argsN.GetVal()。
Us::Type()… 展开后为Us0::Type(),Us1::Type(),…UsN::Type()。数组t[]中保存的是变参args中数据对应的数据类型,在数组的最后添加一个CheckArgType::kEnd表示结束了,在打印日志的时候遇到CheckArgType::kEnd就停止打印变参args中的数据。
示例3
中RTC_DCHECK(1 != 1) << "hello world " << 100 << " " << 3.14;会调用Call函数,t[]和变参args内容及其对应关系,如下图:
template <typename... Us>
RTC_NORETURN RTC_FORCE_INLINE static void CallCheckOp(const char* file, const int line, const char* message, const Us&... args)
{static constexpr CheckArgType t[] = { CheckArgType::kCheckOp, Us::Type()...,CheckArgType::kEnd };FatalLog(file, line, message, t, args.GetVal()...);
}
RTC_DCHECK宏
调用上面的Call函数,除了RTC_DCHECK宏
其他两个参数的宏(如RTC_DCHECK_EQ宏
),都调用CallCheckOp函数,因为两个参数的宏,需要把宏的两个参数都打印出来,所以需要特殊处理。
在格式数组t[]中,首先放入CheckArgType::kCheckOp,在打印日志的时候用于指示将变参args 中的前两个参数按照指定的格式打印。
示例2
中RTC_DCHECK_EQ(100, 200) << “100 is not equal to 200”;会调用CallCheckOp函数,t[]和变参args内容及其对应关系,如下图:
LogStreamer<T, Ts…>
RTC_FORCE_INLINE LogStreamer(T arg, const LogStreamer<Ts...>* prior): arg_(arg), prior_(prior) {}
这个变参模板类有两个数据成员,T
都会推导为Val类型,arg_中保存的是日志数据及其类型(如保存的是"hello world"和CheckArgType::kCharP),prior保存的是LogStreamer对象。
template <typename... Us>
RTC_NORETURN RTC_FORCE_INLINE void Call(const char* file, const int line, const char* message, const Us&... args) const
{prior_->Call(file, line, message, arg_, args...);
}
Call函数内会调用其数据成员的Call函数,在调用时,最重要的一点是把其成员函数arg_当做参数传递下去,prior_->Call函数会把实参arg_, args…映射到其形参const Us&… args上,即一个参数和一个变参组合一下赋给一个变参。
FatalLogCall类
template <bool isCheckOp>
class FatalLogCall final
{public:FatalLogCall(const char* file, int line, const char* message): file_(file), line_(line), message_(message) {}template <typename... Ts>RTC_NORETURN RTC_FORCE_INLINE void operator&(const LogStreamer<Ts...>& streamer) {isCheckOp ? streamer.CallCheckOp(file_, line_, message_) : streamer.Call(file_, line_, message_);}private:const char* file_; /*文件名*/int line_; /*行号*/const char* message_; /*宏函数的参数*/
};
FatalLogCall类重载了&运算符,这个运算符接收LogStreamer类对象streamer,并且将其成员数据file_、line_、message_传递到streamer对象,将这些数据的打印交由streamer对象处理。
FatalLogCall类是一个模板类,但有些特殊,模板参数不使用类型,而使用true、false。示例如下:
#include <iostream>using namespace std;template <bool b>
class A
{
public:void func(){if (b)cout << "b is true" << endl;elsecout << "b is false" << endl;}
};int main()
{A<true> at;at.func();A<false> af;af.func();return 0;
}
RTC_DCHECK宏函数
说了以上的内容,或许还是很抽象,特别是LogStreamer类。现在结合具体的宏,将宏的运算过程一步步展开,从具体的应用中感受整个断言是如何工作的。
宏函数原型
#define RTC_DCHECK(condition) RTC_CHECK(condition)#define RTC_CHECK(condition) \while (!(condition)) \rtc::webrtc_checks_impl::FatalLogCall<false>(__FILE__, __LINE__, \#condition) & \rtc::webrtc_checks_impl::LogStreamer<>()
宏函数的使用
RTC_DCHECK(1!= 1) << "hello world";
在Linux上通过g++ -E命令将宏展开以后得到:
while (!(1!= 1)) rtc::webrtc_checks_impl::FatalLogCall<false>("main.cc", 7, "1!= 1") & rtc::webrtc_checks_impl::LogStreamer<>() << "hello world";
精简一下,去掉命名空间:
FatalLogCall<false>("main.cc", 7, "1!= 1") & LogStreamer<>() << "hello world";
<<运算符的优先级比&运算符高,所以先计算LogStreamer<>() << “hello world”,再计算&运算符。
<<运算符的运算过程
LogStreamer<>() << "hello world"语句的运算过程:
- LogStreamer<>() :定义了一个临时对象。
- 临时对象<< “hello world”:临时对象调用<<运算符。
- operator<<():会把临时对象和"hello world"生成LogStreamer<T, Ts…>对象,并将其返回。
执行过程如下图所示:
&运算符的运算过程
LogStreamer<>() << "hello world"的运算返回LogStreamer<T, Ts…>对象,LogStreamer<T, Ts…>对象和FatalLogCall<false>(“main.cc”, 7, “1!= 1”) &组成新的表达式FatalLogCall<false>(“main.cc”, 7, “1!= 1”) & LogStreamer<T, Ts…>。
FatalLogCall<false>(“main.cc”, 7, “1!= 1”) 会生成一个临时对象,临时对象会调用FatalLogCall类的operator&()函数,其中isCheckOp
为false。处理过程如下图:
RTC_DCHECK(1 != 1) << "hello world " << 100 << 3.14;的执行过程
RTC_DCHECK(1 != 1) << "hello world " << 100 << 3.14;
宏展开后:
while (!(1 != 1)) rtc::webrtc_checks_impl::FatalLogCall<false>("main.cc", 7, "1 != 1") & rtc::webrtc_checks_impl::LogStreamer<>() << "hello world " << 100 << 3.14;
去掉命名空间后:
while (!(1 != 1)) FatalLogCall<false>("main.cc", 7, "1 != 1") & LogStreamer<>() << "hello world " << 100 << 3.14;
根据运算符的优先级,<<运算符比&运算符优先级高,所以先算<<运算符。<<运算符结合性是从左到右。整个语句的执行过程如下:
先计算<<运算符
-
先计算LogStreamer<>() << "hello world ",LogStreamer<>()生成临时对象,临时对象会调用operator<<()函数,operator<<()函数会把"hello world "和临时对象作为参数,生成LogStreamer<T, Ts…>对象,这个对象存储着"hello world "和临时对象。
-
上一步生成的LogStreamer<T, Ts…>对象会继续调用operator<<()函数,同时把自己和100传入,生成一个新的LogStreamer<T, Ts…>对象。一直这样递归下去,直到所有的<<运算符处理完毕。
-
每个<<运算符的参数都用LogStreamer<T, Ts…>进行包装,包装的效果如下图:
再计算&运算符
-
上一步最后会返回LogStreamer<T, Ts…>对象,组成了新的表达式FatalLogCall<false>(“main.cc”, 7, “1 != 1”) & LogStreamer<T, Ts…>。FatalLogCall<false>(“main.cc”, 7, “1 != 1”) 会生成临时对象,临时对象会继续调用operator&()函数。
-
在operator&()函数中会使用LogStreamer<T, Ts…>对象调用Call()函数,会产生递归调用,每次调用都会把本类保存的日志数据往下层传递。
-
递归到最后,LogStreamer<>()生成临时对象会调用FatalLog()函数,将所有日志数据打印出来。
-
这个递归过程和上面的递归正好相反,上面的递归过程是把日志数据一层一层的包装起来,这里的递归是将包装的数据一层一层的取出来,最后将所有日志数据打印出来。之所以采用这种方式,是因为<<运算符的调用次数是未知的。
-
解包打印过程如下图:
RTC_DCHECK_EQ宏函数
宏函数原型
#define RTC_DCHECK_EQ(v1, v2) RTC_CHECK_EQ(v1, v2)#define RTC_CHECK_EQ(val1, val2) RTC_CHECK_OP(Eq, ==, val1, val2)#define RTC_CHECK_OP(name, op, val1, val2) \while (!rtc::Safe##name((val1), (val2))) \rtc::webrtc_checks_impl::FatalLogCall<true>(__FILE__, __LINE__, \#val1 " " #op " " #val2) & \rtc::webrtc_checks_impl::LogStreamer<>() << (val1) << (val2)
宏函数的使用
RTC_DCHECK_EQ(100, 200) << "100 is not equal to 200";
展开后:
while (!rtc::SafeEq((100), (200))) rtc::webrtc_checks_impl::FatalLogCall<true>("main.cc", 7, "100" " " "==" " " "200") & rtc::webrtc_checks_impl::LogStreamer<>() << (100) << (200) << "100 is not equal to 200";
为了方便看,去掉命名空间:
while (!rtc::SafeEq((100), (200))) FatalLogCall<true>("main.cc", 7, "100" " " "==" " " "200") & LogStreamer<>() << (100) << (200) << "100 is not equal to 200";
以上的宏的展开,是在Linux上通过g++ -E命令展开的。说一下在宏展开时需要注意的地方。
宏函数的参数必须被隔离出来,才能被替换,普通的隔离符有空格、括号、运算符,或者专属隔离符##
。##
解决了黏连字符的处理问题。在此处,宏函数的参数name为EQ,则!rtc::Safe##name((val1), (val2))替换后为!rtc::SafeEQ((val1), (val2))。其中SafeEQ是一个宏函数,可以继续展开,在这里就不继续展开了,以后会写一篇文章单独介绍。现在看看这些宏的使用方式:
#include <iostream>
#include "safe_compare.h"using namespace std;
using namespace rtc;int main()
{cout << boolalpha;bool ret = SafeEq(100, 100);cout << "100 == 100 ?,result = " << ret << endl;ret = SafeNe(100, 200);cout << "100 != 200 ?, result = " << ret << endl;ret = SafeLt(100, 150);cout << "100 < 150 ?, result = " << ret << endl;ret = SafeLe(100, 100);cout << "100 <= 100 ?, result = " << ret << endl;ret = SafeGt(100, 200);cout << "100 > 200 ?, result = " << ret << endl;ret = SafeGe(100, 200);cout << "100 >= 200 ?, result = " << ret << endl;return 0;
}
#val1 " " #op " " #val2会被替换为"100" " " “==” " " “200”,其中#
的用法,在《WebRTC源码分析之定位-Location》有介绍。
FatalLogCall<true>(“main.cc”, 7, “100” " " “==” " " “200”) & LogStreamer<>() << (100) << (200) << “100 is not equal to 200”;,带有两个参数的宏,会把两个参数的值也会打印出来。RTC_DCHECK
宏不需要单独处理参数,其他的宏都需要将参数单独打印出来。为了在生成FatalLogCall对象的时候区别开来,RTC_DCHECK
宏生成FatalLogCall对象时,其值为false,其余宏生成FatalLogCall对象时,其值为true。
true或false会赋值给isCheckOp,通过 isCheckOp ? streamer.CallCheckOp(file_, line_, message_) : streamer.Call(file_, line_, message_); 这条语句选择不同的处理方式。
小结
整个断言的处理过程,其中比较难理解的是对于<<运算符的处理,通过变参模板,递归地把日志数据包装起来,每层的包装都使用一个类对象。在使用日志数据时,再递归的获取每层包装的日志数据。