添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
玩滑板的黄花菜  ·  Vue.js 3.3.11 + ...·  2 天前    · 
绅士的烈马  ·  Nomad placement ...·  昨天    · 
文质彬彬的充电器  ·  Safely use ...·  11 小时前    · 
豪爽的海龟  ·  How to - parse JSON ...·  11 小时前    · 
酒量大的香烟  ·  How to parse JSON ...·  11 小时前    · 
爱看书的松鼠  ·  我的分享·  2 年前    · 

C++ 有枚举,编译后其值就被转成整数了,有时程序会有输出枚举名字的需求,朴素的做法就是手工一个个写字符串(名字),并实现匹配,比如:

enum class Shape {rectangle, circular}; std::string ToString (Shape s) switch(s) case Shape::rectangle : return "rectangle"; case Shape::circular : return "circular"; default: assert(false && "不依规矩,不成方圆");

这当然很烦人,三个烦人处:一是每种类型都得写一个函数;二是手工打字符串容易打字出错;三是default那个地方很讨厌。

2 破题 __PRETTY_FUNCTION__

好在,g++、clang 都扩展支持一个有趣的宏:__PRETTY_FUNCTION__ , MSVC 则有 __FUNCSIG__ 。在编译时,这个宏会被替换成一个字符串,内容是当前所在函数的“漂亮的/pretty”的名字,比如:

void foo(const char* ) std::cout << __PRETTY_FUNCTION__ << std::endl;

调用 foo(), 它应该输出:

void foo(const char*) 

注意到了吗?入参的类型 const char* 是 __PRETTY_FUNCTION__ 所得的函数名字的组成之一 —— 这并不意外,要区分同名的多个不同函数,必须依靠它的入参组成(入参类型、入参个数),这是C++函数支持重载的重要机制。

嗯,现在可以陷入沉思了……要是有个函数,它入参就是“类型”(不是类型具化后数据),那么传入int类型, 通过 __PRETTY_FUNCTION__ 能得到一个带 “int” 子串的字符串,传入 const char (或 char const),就能得到一个带 “const char” 的字符串,把子串扣出来,我们就得到某个类型的名字了嘛!而如果我们有一个 枚举(enum)类型叫 “Color”,不也一样能扣出它的名字?这距离我们扣枚举的值的名字,似乎很近了。

继续深思,入参是类型,而不是类型具化后的数据,不就是模板函数嘛!

让我们来写一个函数模板,看看这个 __PRETTY_FUNCTION__ 得到的函数名,会是什么呢?

我们给出能完整执行的代码:

#include <iostream> template<typename T> void foo(T t) std::cout << __PRETTY_FUNCTION__ << std::endl; int main() foo(10); foo('c');

它的输出是:

void foo(T) [with T = int]
void foo(T) [with T = char]

注意到了吧!在一对方括号中"with"后面的内容——漂亮地包含了参数的实际类型。

3 试试用户自定义类型

不仅支持C++内置的简单类型,以上代码当然也支持 用户自定义 的struct/class类型的。让我们马上再来试试,这次我们干脆让foo函数返回字符串:

#include <iostream> template<typename T> char const* foo(T t) return __PRETTY_FUNCTION__; struct d2school {}; int main() std::cout << foo( d2school{} ) << std::endl;

刻意解释: d2school {} 用来临时构造一个 d2school 类型的对象,自然,也可以使用C++老传统的 d2chool () 来构造。

输出是:const char* foo(T) [with T = d2school]。

说到用户自定义类型,枚举也是用户自定义类型呀,来,马上试试:

#include <iostream> template<typename T> char const* foo(T t) return __PRETTY_FUNCTION__; struct d2school {}; enum class Color {red, green}; int main() std::cout << foo(d2school {}) << std::endl; std::cout << foo(Color::red) << std::endl;

猜到了吧,输出肯定是:

const char* foo(T) [with T = d2school]
const char* foo(T) [with T = Color]

前面刻意解释 的“ d2school {} ”暴露了一个小问题:为了得到一个类型的名字,我们却不得不临时提供一个数据,正好有个数据时倒好办,手上没数据强行搞一个变量这就烦人了。

小问题倒是好解决,C++模板允许我们精确地指定一个模板的类型的入参。下面我们去掉foo的入参,再对应修改调用方法:

#include <iostream> template<typename T> char const* foo() // 入参去掉了…… return __PRETTY_FUNCTION__; struct d2school {}; enum class Color {red, green}; int main() // 调用起来很酷: std::cout << foo<unsigned int>() << std::endl; std::cout << foo<bool>() << std::endl; std::cout << foo<d2school>() << std::endl; std::cout << foo<Color>() << std::endl;

嗯,现在的代码用起来和看起来都非常的有C++的味道(毕竟我们都用过 C++自带的那些xxx_cast 不是?)

能跨各主流编译器(g++\clang\MSVC),并且是在编译期间得到指定类型的名字字符串,在很多时候也是蛮有用的。但是,我们不仅想要枚举类型的名字,我们还想要枚举值的名字。

这世上还有什么语言,能像C++那样对模板(泛型)支持到令人发指的程度呢?C++模板的又一个简单而基础的知识点来了:模板支持非类型的入参呢!大白话点讲,就是模板入参不仅可以传类型,也可以假装“退回”普通函数,传一个具体的数据,而枚举值,就是一个数据。

如果你还不明白,就再看一眼前面代码中我们定义的Color枚举, Color 是一个枚举类型,而其下定义的 red、green,那是值。
虽说支持传递非类型参数 是C++模板一项“令人发指”的特性,但又有细分:在当前主流的 C++1x 标准下,它是翘兰花指,而在C++20,这项特性是疯狂到赤裸裸“竖中指”的地步——C++1x (11、14、17)传的数据只能是简单类型,而C++20以后,竟然可以传用户自定久的struct/class类型的数据了……

我们使用C++1x 标准就够了,因为enum 值 底层实现是整数系类型,属于简单类型。

4 两个模板参数:类型和该类型的数据

现在我们要为 foo 模板 添加第二个模板入参,并且它是第一个入参(T,一个类型)的数据,让我们为它取名为“V”:

template <typename T, T V> // “T”是类型, “V” 是 T 类型的一个数据 char const* foo() return __PRETTY_FUNCTION__;

注意:T 前面 有个 typename(“type name”),明确指明T是一个类型(的名字),而V,它的前面是T,所以它是一个T类型的数据。

既然模板入参多了一个,调用时,自然也得多传一个,比如这样:foo<bool, false>() ; 其中,bool 是一个类型,而 false 是一个bool类型的数据。完整代码如下:

#include <iostream> template<typename T, T V> char const* foo() return __PRETTY_FUNCTION__; enum class Color {red, green}; int main() std::cout << foo<unsigned int, 999u>() << std::endl; std::cout << foo<bool, false>() << std::endl; std::cout << foo<Color, Color::red>() << std::endl;

注意:我们去掉了 d2shool 结构的相关代码,原因见前。
我们迫切关心的是:__PRETTY_FUNCTION__ 这家伙,它将如何展现第二个模板参数的内容,请看以上代码的输出:

const char* foo() [with T = unsigned int; T V = 999]
const char* foo() [with T = bool; T V = false]
const char* foo() [with T = Color; T V = Color::red]

__PRETTY_FUNCTION__,你太棒了!竟然把值都给包含进去了……

看到输出内容中的“Color::red”了吧?现在,想办法把它从上面的字符串中扣出来,文章就结束了。

5 编译期“扣”字符串

扣的方法很多——但关键是得在编译期间扣——“现代”的C++的又一知识点来了:带有constexpr 修饰的函数,编译器将会在编译代码时就执行它,得到结果后,把结果塞入代码以替换这个函数的执行——听起来像是加强版的“inline”修饰的函数;但后者只是尝试将函数内的实现提出来变成每一处调用位置的代码,constexpr 却是尝试先编译一下这个函数,然后再将编译后的这个函数执行一下,得到结果后再替换代码。

举个例子吧,假设,你有一个函数:

constexpr int accumulate (int beg, int end) int r = 0; for (int i=beg; i<=end; ++i) r += i; return r;

逻辑很简单:从 beg 一直加到 end,返回累加和。然后你这么调用:

int sum = accumulate (1, 100);

由于 accumulate 带有 constexpr 修饰,所以编译器会在编译时——此时你的可执行程序还不存在——就直接执行 accumulate(1, 100),然后在内心骂你一句“小傻瓜,这不就是 1加到100嘛!”,于是(可以先简单地认为)它帮你改写了调用处的源代码,变成:

int sum = 5050

然后再编译。

——这已经不是兰花指或中指的问题了,依我看这是举或不举的问题。同样,这里也有版本高低之分。C++11标准能支持constexpr函数有很大限制,比如无法支持如上带有循环的代码;但C++14或更高标准则能支持(当然也有不少限制)。

牛皮吹完……现在必须强调一下,constexpr 并不强制,或者说,并不保证经它修饰的函数,一定能在编译器执行求值,你必须依据C++标准的规定很小心地写函数,否则,标示了constexpr的函数,依然可能是在运行时调用。

也可以将上述现象视为constexpr的一种灵活性;你可以拿它和C++20的consteval 作对比。

回归主题,如何从母串:

“const char* foo() [with T = Color; T V = Color::red]”

当中扣出“Color”、“Color::red”、“red” 这三个子串呢?表面上看,这太简单了,基本和现代C++的新鲜知识点无关了;但正如前面所强调的,如何写这一实现,事实上必须非常小心,甚至往往得借助一些工具查看编译后的汇编代码,才能确实是否真的实现了编译期求值。

我的扣法是:找到母串中的 ‘=’(两个) 、’;’ 、 ‘:’ (第二个)以及最后结束的 ‘]’ 等字符在母串中的位置(下标索引),然后:

  • 首个 ‘=’ 和 ‘;’ 之间的,是枚举的类型名字,本例是 Color;
  • 第二个’=’ 和 ‘]’ 之间的,是枚举值的全称,本例是 Color::red;
  • 冒号和 ‘]’ 之间的,是枚举值的短名字,本例是 red。
  • 实际处理时,还需要跳过等号后面的一个空格。另外,我们也得支持传统C++的枚举(即不带class),此时,全称和短名字是相同的。判断方法:没冒号就是传统的枚举,有冒号就是新标的枚举——专业术语叫 “scoped enum”。

    连实现带用例的完整代码见后。

    一点说明:为了省除手工写代码,我用了C++17的string_view。因此下面的代码必须使用支持C++17标准的编译器;事实上,于g++而言,编译器自身版本也得足够新,建议11.x以上。

    如果不懂如何查看自己用的C++编译器支持的C++版本,可以通过类似以下代码查看相关版本:

    cout << __cplusplus << " , " << __VERSION__ << endl;

    我的输出是:201703,11.2.0

    6 完整代码

    #include <cassert> #include <cstring> #include <iostream> #include <string_view> // 用来存储枚举反射信息的结构体 // 注意名字都使用 string_view 存储,以避免动态内存分配 struct ReflectionEnumInfo bool scoped; // 是否 scoped enum std::string_view name, valueFullName, valueName; // 类型名、值名、值全名 // 构造时,从母串中按指定位置 得到各子串 // info : 母串,即 __PRETTY_FUNCTION__ 得到的函数名 // e1:等号1位置; s:分号位置; e2:等号2位置; colon:分号位置; end:]位置 constexpr ReflectionEnumInfo(char const* info , std::size_t e1, std::size_t s, std::size_t e2 , std::size_t colon, std::size_t end) : scoped(colon != 0), name (info + e1 + 2, s - e1 -2) , valueFullName (info + e2 + 2, end - e2 - 2) , valueName((scoped)? std::string_view(info+colon+1, end-colon-1) : valueFullName) // 说了半天的 模板函数,带 constexpr template <typename E, E V> constexpr ReflectionEnumInfo Renum() char const* info = __PRETTY_FUNCTION__; // 找各个符号位置 std::size_t l = strlen(info); std::size_t e1 = 0, s = 0, e2 = 0, colon = 0, end = 0; for (std::size_t i=0; i<l && !end; ++i) switch(info[i]) case '=' : (!e1) ? e1 = i : e2 = i; break; case ';' : s = i; break; case ':' : colon = i; break; case ']' : end = i; break; return {info, e1, s, e2, colon, end}; ////// 下面是用例代码 /////// // 为方便输出 ReflectionEnumInfo ,重载一下输出流操作 std::ostream& operator << (std::ostream& os, ReflectionEnumInfo const& ri) os << "scoped = " << std::boolalpha << ri.scoped << std::noboolalpha << "\nname = " << ri.name << "\nvalueName = " << ri.valueName << "\nvalueFullName = " << ri.valueFullName << "\n------------------------------\n"; return os; enum class Shape {rectangle, circular}; enum Color {cRed = 1, cGreen }; int main() auto ri1 = Renum<Shape, Shape::rectangle>(); std::cout << ri1; auto ri2 = Renum<Shape, Shape::circular>(); std::cout << ri2; auto ri3 = Renum<Color, cRed>(); std::cout << ri3; auto ri4 = Renum<Color, static_cast<Color>(2)>(); std::cout << ri4; auto ri5 = Renum<Color, static_cast<Color>(12)>(); std::cout << ri5; std::cout << std::endl;

    请特别注意最后两个测试案例,它们都是从整数值强制转换到Color枚举,但一个是合法范围,另一个是非法范围。

    另外,请注意,这里使用了 gcc 的一处扩展:gcc 提供了静态版的 strlen()库函数 ,即:给出一个编译期的C风格(以零结束)的字符串,就能在编译期直接“数”出来这个字符串的长度。如果不利用这个扩展函数的话,得自己写一个编译期的 strlen()。

    scoped = true
    name = Shape
    valueName = rectangle
    valueFullName = Shape::rectangle
    ------------------------------
    scoped = true
    name = Shape
    valueName = circular
    valueFullName = Shape::circular
    ------------------------------
    scoped = false
    name = Color
    valueName = cRed
    valueFullName = cRed
    ------------------------------
    scoped = false
    name = Color
    valueName = cGreen
    valueFullName = cGreen
    ------------------------------
    scoped = false
    name = Color
    valueName = (Color)12
    valueFullName = (Color)12
    -------------------------------
    

    在线编译、运行及查看结果 :Coliru Viewer

    从程序自身的运行输出结果看,似乎没有错,但是,“Renum()”函数到底是不是只在编译器执行呢?可借助工具站点(详见文末)查看代码的汇编结果。其中的重点在于main()函数内的第一行代码: