WebRTC源码分析之断言-RTC_DCHECK

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中的数据。

示例3RTC_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 中的前两个参数按照指定的格式打印。

示例2RTC_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类是一个模板类,但有些特殊,模板参数不使用类型,而使用truefalse。示例如下:

#include <iostream>

using namespace std;

template <bool b>
class A
{
public:

	void func()
	{
		if (b)
			cout << "b is true" << endl;
		else
			cout << "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命令展开的。说一下在宏展开时需要注意的地方。

宏函数的参数必须被隔离出来,才能被替换,普通的隔离符有空格、括号、运算符,或者专属隔离符####解决了黏连字符的处理问题。在此处,宏函数的参数nameEQ,则!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_); 这条语句选择不同的处理方式。

小结

整个断言的处理过程,其中比较难理解的是对于<<运算符的处理,通过变参模板,递归地把日志数据包装起来,每层的包装都使用一个类对象。在使用日志数据时,再递归的获取每层包装的日志数据。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值