本文源自于今天对 neuront 童鞋的这篇文章的末尾的那段代码的 C++ 实现的思考。(好多「的」……)
尽管 std::accumulate()
和 Python 的 reduce()
类似,但因为 C艹 的 std::map<std::string, std::map<std::string, int>>
和 std::map<std::string, int>
是不同的类型,所以似乎只能自己用可变参数模板写一个了。
简单起见,我们还是退一步,来解决一个更简单、更不通用、而且似乎和 reduce()
完全无关的问题吧:如何才能一次性取得任意层次的字典值?用更直白的代码表达,就是我们需要一个 GetMapValue()
函数,实现这样的功能:
// 用于缓解眼花缭乱感的宏
#define MAP_LITERAL(...) { __VA_ARGS__ }
// 简单映射
std::map<std::string, std::string>
simple_dict = MAP_LITERAL({"Hello", "World"});
// 「我勒个去居然这么麻烦」映射
std::map<std::string,
std::map<std::string,
std::map<std::string,
int>>>
nested_dict = MAP_LITERAL( { "x", MAP_LITERAL( { "y", MAP_LITERAL( { "a", 10 } ) },
{ "z", MAP_LITERAL( { "b", 20 } ) } ) });
auto value1 = GetMapValue(simple_dict, "Hello");
std::cout << value1 << std::endl;
auto value2 = GetMapValue(nested_dict, "x", "y", "a");
std::cout << value2 << std::endl;
初始的版本
初步分析,GetMapValue()
需要接受一个 Map 以及至少一个 Key。如此,参数可变,首选 C++11 的可变参数模板。一次性将所有 Key 拿到手后,每次用第一个 Key 获取下一级 Map,而后用余下的 Key 递归,最终获取所要的值。而要写递归,最好先从最简单的情况写起。
template <typename MapType>
auto GetMapValue(MapType const& map, typename MapType::key_type const& key)
-> typename MapType::mapped_type
{
return map.at(key);
}
鉴于本人目前对于右值引用还不熟悉,就只用 const&
了(&
只能引用左值;const&
既能引用左值、又能引用右值,但无法修改)。
这里需要注意的是 STL 的几种 Map 里 typedef
到的数据类型,略坑:
key_type
是 Key 的类型
mapped_type
是 Value 的类型
value_type
是 Map 中实际存储的键值对的具体类型(如 std::pair<key_type, mapped_type>
)
不过这样一来,我们的 GetMapValue()
就可以在 STL 的这几种 Map 里通用了。
不被支持的递归 decltype
模板
本来我想,递归版本按照上面这个最简版本写一下就 OK 了。可变参数模板的基本用法我以前的日志有写过;这里唯一麻烦的是返回值,因为通用性,只有最终被调用的版本才知道确切的返回值,不过 decltype
似乎可以救场:
template <typename MapType, typename... MoreKeyTypes>
auto GetMapValue(MapType const& map, typename MapType::key_type const& key,
typename MapType::mapped_type::key_type const& anotherKey, MoreKeyTypes... moreKeys)
-> decltype(GetMapValue(map.at(key), anotherKey, moreKeys...))
{
return GetMapValue(map.at(key), anotherKey, moreKeys...);
}
然而,编译器不给面子,直接提示模板推导失败,decltype
时找不到接受两个 Key 版本的 GetMapValue()
(三个参数)。
似乎,decltype
是不支持递归调用的,亦或者推导时自身还不存在。decltype
以及模板的一些规则真心还不是很熟,所以暂时没有找到真凭实据。
Traits
于是我终于知道为什么世界上会有 Traits 这种东西存在了。(比如 std::basic_string
的模板参数之一就是 class Traits = std::char_traits<CharT>
。)Traits 是用来描述某个类型周边信息的东西。这里,我们需要一个 Traits 来计算一个 Map 被使用若干次 Key 后的 Value 类型。
那么很显然的,这个 MapTraits
依旧是可变参数模板、依旧是递归实现。
template <typename MapType, typename KeyType, typename... KeyTypes>
struct MapTraits
{
typedef typename MapTraits<typename MapType::mapped_type, KeyTypes...>::mapped_type mapped_type;
};
template <typename MapType, typename KeyType>
struct MapTraits<MapType, KeyType>
{
typedef typename MapType::mapped_type mapped_type;
};
完全体
不出所料,最终的代码是一番如此纠结的景象。看这样的代码眼睛压力山大。
template <typename MapType>
auto GetMapValue(MapType const& map, typename MapType::key_type const& key)
-> typename MapType::mapped_type
{
return map.at(key);
}
template <typename MapType, typename... MoreKeyTypes>
auto GetMapValue(MapType const& map, typename MapType::key_type const& key,
typename MapType::mapped_type::key_type const& anotherKey, MoreKeyTypes... moreKeys)
-> typename MapTraits<typename MapType::mapped_type, typename MapType::mapped_type::key_type, MoreKeyTypes...>::mapped_type
{
return GetMapValue(map.at(key), anotherKey, moreKeys...);
}
完整的测试代码见这个 Gist:https://gist.github.com/timothyqiu/6877974
Traits 的小插曲
我第一次写的时候,把 MapTraits
的特化形式写成了这样:
template <typename MapType> // 正式版本: typename MapType, typename KeyType
struct MapTraits<MapType, typename MapType::key_type> // 正式版本: MapType, KeyType
{
typedef typename MapType::mapped_type mapped_type;
};
其实只是用 typename MapType::key_type
替换了一个正式版本里的模板参数 KeyType
而已。这样做其实也并非不可,只不过产出的是「高标准、严要求」的代码:
// 正式版本可行,但是在这个版本里一堆模板错误
auto value2 = GetMapValue(nested_dict, "x", "y", "a");
// 两个版本都可行
auto value2 = GetMapValue(nested_dict, std::string("x"), std::string("y"), std::string("a"));
原理大致是,这个版本因为把特化形式的第二个模板参数固定成了第一个参数的 key_type
,所以如果使用了和 key_type
不同但是可以隐式转换的类型,会导致模板推导失败。
C 语言中的字符串
抽象数据类型
C 语言贴近机器模型,所以并不提供字符串数据类型。但是,字符串却在 C 语言中作为 ADT 存在着:使用字符指针代表字符串,同时提供了 string.h
作为字符串的操作库,而在实际存储时则使用字符数组。
虽说单引号引起的字符是该字符对应的一个整型常量,但是 'A'
和 65 没有任何关系。因为 C 语言并没有要求每个实现都用 ASCII 字符集作为执行字符集。
事实上,C 语言标准要求每个 C 语言实现都要定义两个字符集:代码字符集、执行字符集。每个字符集又都由基础字符和扩展字符构成。扩展字符可有可无,完全由该实现所支持的区域设置决定;基础字符只划定了字符,字符对应的码位留由实现决定。
字符与 char
类型
字符集顾名思义就是字符的集合,字符集除了划定所包含的字符外,还将其所包含的字符按照一定顺序编号,这样的编号称作码位。著名的 ASCII 参照已有电报报文设计,分别用数字 0 到 127 作为英文大小写字母、阿拉伯数字、标点符号等的码位。由于出现早、使用广,后辈字符集、字符编码几乎都与之兼容为荣。
然而 ASCII 本身并不完美,尽管名字叫「美国信息交换标准代码」,但是却连英语中的一些诸如 naïve、café、élite 的外来词都无法表示,欧洲各国那些稀奇古怪的字母就更别提了。所幸,我们很快来到了一个字节 8 位的年代,char
能够容纳 0 到 255 一共 256 个值,于是各国纷纷物尽其用,将一个字节中 ASCII 并没有用到的 128 到 255 对应为自己国家所需要用到字符。这些五花八门的对应规则便是后来的代码页(code page),同样的值可以在不同的代码页中表示不同的字符。
到目前为止,char
类型还算是名副其实的字符类型。然而,东亚文化圈广泛使用的汉字成千上万,少说也需要上千个码位,一个字节所能提供的这区区 256 个码位实在是杯水车薪。
计算机科学里的任何问题都可以通过增加一个中间层来解决。
既然码位不得不超过 255,一个字节放不下,那么将以前的「码位→字节」变成「码位→编码→字节序列」如何?
举个栗子:Unicode 字符集中,字符「汉」的码位是 27721(十六进制 6C49),一个字节显然容不下,我们可以通过 UTF-8 编码将其变成字节序列。
- 通过字符集找到对应码位:汉→
U+6C49
;
- 根据 UTF-8 编码规则,介于
U+0800
和 U+FFFF
之间的码位需要按照 1110xxxx 10xxxxxx 10xxxxxx
的形式编码为三个字节;
6C49
的二进制形式是 0110 1100 0100 1001
,代入上面的模板得到 11100110 10110001 10001001
;
- 得到最终的字节序列
E6 B1 89
。
看上去不错,但这样一来,char
这个「字符」类型就有点名不副实了:一个 char
可能只是一个字符的一部分,只有两个或者更多的 char
拼起来才能够表示一个字符。更为麻烦的是,相关的标准库也变得不再可靠了:类似 strlen()
这种 too simple, sometimes naive 的方法顿时从返回字符串的字符数降级成了返回字符串的字节数。
printf("%zu\n", strlen("\xE6\xB1\x89")); // 输出 3;但实际上只有一个字:汉
多字节字符串
为了解决 strlen()
的这种尴尬,C 语言中引入了「多字节字符串」(multibyte string)的概念,一个字符可以对应多个字节;而原先的字符串则对应称为「字节字符串」(byte string),一个字符只能对应一个字节。
char const *multibyteString = "Hello 世界"; // 和普通字符串没什么区别
多字节字符串依旧是 char *
,所以这两者的区别其实只是抽象概念上的,何况严格来说,字节字符串也是一种特殊的多字节字符串。
不同的编码每个字符占用的字节数可能不同,即便是同一种编码,不同的字符也可能占用不同的字节数。所以多字节字符串面临的主要问题便是「下一个字符到底占几个字节」。C 语言引入了 mblen()
函数来回答这个问题:
int mblen(char const *s, size_t n);
其中 s
是一个多字节字符串,而 n
表示至多检查多少个字节;这个函数会根据当前区域设置(Locale)的 LC_CTYPE
分类中所指定的编码来解析这个多字节字符串,返回该字符串的字一个字符占几个字节。
char const *multibyte= "\xE6\xB1\x89"; // 「汉」的 UTF-8 编码
printf("%d\n", mblen(multibyte , 5)); // 输出 -1;因为默认区域设置为 C,无法识别这样的序列。
setlocale(LC_CTYPE, "en_US.UTF-8"); // 非 Windows 平台用,Windows 平台不支持 UTF-8 的区域设置
printf("%d\n", mblen(multibyte, 5)); // 输出 3;因为当前区域设置为 UTF-8 编码。
宽字符 wchar_t
多字节字符串的引入终于算是「一定程度上解决了问题」,但这种做法实在繁琐。于是,C 语言同时引入了宽字符类型 wchar_t
和配套支持库 wchar.h
,试图一劳永逸地解决这个问题。
wchar_t const *wideString = L"Hello World"; // 使用前缀 L
与 char
的一个字节不同,wchar_t
类型本身可以占多个字节,保证永远与字符一一对应,省下了编码的步骤。如此就可以通过 wchar.h
中提供的 wcslen()
函数正确计算 wchar_t *
字符串中的字符数了。
多字节字符串和宽字符串的转换
与 mblen()
类似,C 语言提供了两组共四个函数,根据当前的区域设置进行多字节字符(串)和宽字符(串)的转换。
wctomb()
将宽字符转换为多字节字符
mbtowc()
将多字节字符转换为宽字符
wcstombs()
将宽字符串转换为多字节字符串
mbstowcs()
将多字节字符串转换为宽字符串
误区
看到这里,你是否想过这个能够保证与字符一一对应的 wchar_t
类型到底需要占几个字节呢?没错,不一定占多少,但总之至少一个字节就对了!
事实上,关于 wchar_t
的准确要求是:能够容纳下所支持的区域设置中,最大的扩展字符集中的每一个码位。
an integer type whose range of values can represent distinct codes for all
members of the largest extended character set specified among the supported locales.
也就是说,如果有一个只支持 ASCII 字符集的奇葩的 C 语言实现存在,那么在这个实现下,wchar_t
完全可以只占 8 位。
如此,我们经常听到的「宽字符和 Unicode 有关」的说法也就不攻自破了。
C11 中引入的定长字符类型
时过境迁,转眼到了 2011 年。正如 C99 中在 stdint.h
中加入了 uint32_t
之类的定长整数类型一样,C11 也引入了两种定长字符类型以满足 Unicode 的需要:
char16_t
占 16 位,2 字节,可以保存 UTF-16 字符。
char32_t
占 32 位,4 字节,可以保存 UTF-32 字符。
同时,继 string.h
、wchar.h
后,又进入了 uchar.h
作为这两种字符类型的支持库。
char16_t *utf16String = u"Hello UTF-16"; // 使用前缀 u
char32_t *utf32String = U"Hello UTF-32"; // 使用前缀 U
C++ 中的字符串
C++ 极尽所能兼容 C 语言,陆续支持了和 C 语言相同的那些 char
、wchar_t
、char16_t
和 char32_t
类型,这里就不再重复了。
至于 std::string
,用 C++ 不可能没听说过,它封装了常用的字符串操作。但你是否知道 std::wstring
的存在呢?
如果有心,你会发现 std::string
实际上是对 std::basic_string<T>
模板的特化,形式类似于:
typedef std::basic_string<char> std::string;
typedef std::basic_string<wchar_t> std::wstring;
至于 C++11 中,由于 char16_t
和 char32_t
的引入,也对应增加了 std::u16string
和 std::u32string
的特化版本。
std::basic_string::data()
在 C++11 以来的改变
std::basic_string<T>
其实只是对 T
字符数组的封装,如果需要使用 C 风格的字符串 T *
,可以通过 c_str()
方法获取。
而如果说 c_str()
方法表示「获取该类对应的 C 字符串表示」的语义,那么 data()
方法则表示「获取该类实际保存的数据」的语义。
在 C++11 之前,通过 data()
方法获取的 T *
并不保证 NUL
结尾,即仅保证 [data(), data() + size())
有效。而自 C++11 起,data()
变为保证 NUL
结尾,与 c_str()
的行为完全相同。
从 C++17 开始,我们还可以 data()
方法获取可修改的缓冲。
Windows API 中的字符串类型
不得不说,Windows 的各种大写的自定义类型实在丑陋得令人发指。经常不由得让人发出「好好起名字会死么?」的感叹。
#define CHAR char
#define LPSTR char *
#define LPCSTR char const *
#define WCHAR wchar_t
#define LPWSTR wchar_t *
#define LPCWSTR wchar_t const *
当然,微软如此做法自然有其历史原因,我对所谓的「向前兼容」表示非常无奈。不过话说回来,除了 Windows API 如此大张旗鼓地使用 wchar_t
,其余地方真不怎么能见到宽字符的身影。现代 Windows 内部使用 UTF-16,所以 wchar_t
能够保证是 16 位的。
Windows API 中所有与字符串相关的函数、数据结构等,都存在 ANSI 和 Unicode 两个版本,可以通过宏 UNICODE
来切换。比如一个普通的 MessageBox()
函数,实际定义是这样的:
int MessageBoxW(HWND hWnd, LPCWSTR text, LPCWSTR caption, UINT type); // 宽字符串版本
int MessageBoxA(HWND hWnd, LPCSTR text, LPCSTR caption, UINT type); // 多字节字符串版本
#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
#endif
字符类型也是类似,使用 TCHAR
作为一个通用的字符名字。字符常量则使用 TEXT()
宏来定义。
#ifdef UNICODE
#define TCHAR WCHAR
#define TEXT(s) L ## s
#else
#define TCHAR CHAR
#define TEXT(s) s
#endif
如果你用的是 Visual C++,这个 UNICODE
宏通常并不需要手动设置,而是作为工程属性的一员出现的。
p.s. 不要被这个宏的名字骗了,还记得之前我们说过的 wchar_t
和 Unicode 一点关系都没有吗?可以这么理解:wchar_t
只在 Windows API 中表示 UTF-16 数据。
宽窄字符串的转换
Windows 平台也可以用之前提到的四个函数转换 char
字符串和 wchar_t
字符串。但 Windows 本身提供的 API 似乎更有效率:
MultiByteToWideChar()
多字节字符串转换为宽字符串
WideCharToMultiByte()
宽字符串转换为多字节字符串
再次吐槽 M$ 这蛋疼的命名,命名是转换字符串,起名字起得跟转换单个字符一样。
BSTR
BSTR
可以解释为 Basic String 或者 Binary String,是 Windows 下用于 COM、自动化的字符串类型。定义为:
typedef OLECHAR *BSTR;
OLECHAR
是 Windows 的历史遗留,类似于根据宏 OLE2ANSI
转换的 TCHAR
,但 OLE2ANSI
是 16 位时代的产物,于是 Win32 环境下,OLECHAR
永远是 WCHAR
,也就是 wchar_t
。
尽管如此,BSTR
并不能等同于 OLECHAR *
,因为它们的内存布局不同:BSTR
在它自身前拥有一个四个字节的前缀、以双 NUL
结尾。其中,前缀用于描述字符串内容所占的字节数,它本身所指向的是字符串内容而非长度。
-4 0 2 4 6 8 10
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 6 | H | i | ! | NUL | NUL |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
↑
bString
这一点,从用于创建 BSTR
的接口 SysAllocString()
便可以看出:
BSTR SysAllocString(OLECHAR const *psz);
倘若 BSTR
与 OLECHAR *
相同,那么岂不变成了鸡生蛋、蛋生鸡的问题?
虎头蛇尾
呃……前面讲 C 语言字符串的时候似乎还是觉得挺有说头,足足规划了一个星期之久,但后面似乎越来越不知道该讲什么,留待日后积累到新的东西再完善吧~
以上。
今天在看 C++11 的 Inherited Constructors 特性时发现了一个以前不知道的传统 C++ 奇技淫巧。
class Base {
public:
void foo(float a);
};
class Derived: public Base {
public:
void foo(int a);
};
这样的代码,显然 Derived::foo()
会把 Base::foo()
覆盖。
今天得知,using
居然还可以把父类的成员「提升」到子类中:
class Derived: public Base {
public:
using Base::foo; // 看这里看这里看这里
void foo(int a);
};
如此,就相当于在 Derived
中添加了一个和 Base::foo()
一模一样的成员。(当然,从字面上也是很顾名思义的嘛。)
Derived d;
d.foo(3.14f); // 这样调用的就是 Derived::foo(float a) 了
之前我曾想,两个完全符合「is-a」关系的类,做成聚合显然是不甘心的。但如果父类接口有 N 个,子类只是想添加 1 个接口,然后将父类中的 M 个(M < N)接口暴露出来该怎么办?想想 private
继承,然后自己在子类中写 M 个 wrapper 似乎是个可行的方法,但如果 M 很大似乎依旧不甘心。现在知道了这个技巧似乎好解决了很多呢。(不过,整个类的声明也会随之变得脑残起来。)
顺带一提,传统 C++ 中的这个方法是无法提升构造函数的,而 C++11 中则加入了允许继承(提升)构造函数的特性(当然,目前貌似还没有编译器支持 = =)。
以上。
在派生类构造函数中捕获基类构造函数异常
class Base {
public:
Base()
{
throw std::runtime_error("Error from Base");
}
};
class Derived : public Base {
public:
Derived()
{
try {
} catch(std::exception const& e) {
}
}
};
类似于这样的代码,是捕获不到异常的,因为执行到构造函数的函数体中时,基类已经构造完毕了。
索性 C艹 有一个神奇的写法:
Derived::Derived()
try : Base() {
} catch(...) {
}
没错,我没有漏写大括号……你完全可以直接把一个 try {} catch {}
当作函数体……同理,对于一般函数这么写也行:
void foo()
try {
} catch(...) {
}
当然,构造函数的 catch
里是无论如何都会帮你确保有一个异常被抛出去的。(你不显式抛,它就帮你把被捕获的异常抛出去。)
后来发现这个东西学名叫做:Function-try block。
C++11 的异常处理
查了一番,似乎大的改变在于:
- 废弃原先的 Dynamic Exception Specification
- 添加了
noexcept
代替之
先看看之前的 Dynamic Exception Specification 是什么:
void foo(); // (1) 允许抛出异常
void foo() throw(X, Y); // (2) 只允许抛出 X 和 Y 异常
void foo() throw(); // (3) 不允许抛出异常
坑爹之处在于第 2 点:Dynamic Exception Specification 是在运行时进行的,于是,为了确保只能抛出指定类型的异常,就会生成额外代码降低执行效率。
最扭曲的是可自定义的 unexcepted 处理函数。所谓 unexcepted 处理函数,就是一旦你抛出了 throw(...)
中没有出现的类型,系统会去调用的那个函数。默认的 unexcepted 函数直接调用 std::terminate()
结束程序;如果你觉得这不好,你可能会想要去替换掉这个全局的处理函数,你会发现你可以:
- 和默认行为一样,调用
std::terminate()
自尽。
- 抛出异常,但是:
- 这个异常存在于
throw(...)
列表中,那么正常抛出。
- 这个异常不存在于列表中,但是
std::bad_exception
在,那么抛 std::bad_exception
。
- 这个异常不存在于列表中,而且
std::bad_exception
也不在,那么系统 std::terminate()
之。
重点在于,这是一个「全局」的处理函数。如果有人自定义这个,相当于如果要写 throw
列表,就要把 std::bad_exception
加上。
于是 C++11 因为这个在实践中基本没人用的东西而废弃了这个名字很长的功能,用 noexcept
取而代之:
void foo(); // (1) 允许抛出异常,相当于 noexcept(false)
void foo() noexcept(constant-expression); // (2) expression 成立时不允许抛出异常
void foo() noexcept; // (3) 不允许抛出异常,相当于 noexcept(true)
也就是将原先的 (2) 去掉,然后扩展一下原先的 (3)。一旦不允许抛异常的函数抛出了异常,那么直接 std::terminate()
结束运行。
对应还有一个同名的操作符 noexcept
,用来在编译时判断表达式是否允许抛出异常(遇到函数时只通过其声明时的 noexcept
与否判断)。
noexcept(1 + 2) // false
noexcept(throw 1) // true
noexcept(foo()) // 返回 foo() 声明时的 noexcept 与否
最好在哪里捕获异常
处处都 catch
的话,就反而比 if
麻烦了。据推荐,最好在这些地方 catch
:
- 需要更换错误报告手段(例如:实现 C 接口、进入了
noexcept
函数、多线程、系统回调……)
- 允许这种异常的发生(例如:如果
GetEntryDataFromZip()
会在 zip 文件数据损坏时抛出异常,那么 DisplayImageFileInZip()
就不应该捕获;而 CopyAllEntriesToAnotherZip()
则可以选择捕获这个异常,生成一个空的 zip 文件。)
- 有替代方案
参考内容:Jon Kalb: Exception-Safe Coding in C++
听说这个特性是很久以前了,总是读作「维拉迪克·坦普雷特」,一直没反应过来中文到底该叫什么,因为 C 时代的 Variadic Macro 我一直是很象形地读作「点点点」的 = =||
OK,扯远了。Variadic Template 对应中文应该是「可变参数模板」。
Parameter Pack
既然是可变参数,就需要通过某种方式来表示这些参数,而这里的解决方案就是 Parameter Pack 参数包,不知道可不可以简称「餐包」 =_,=
声明参数包的方法是在类型和名称之间加 ...
:
template<typename... Types> struct Tuple {};
Tuple<> t0; // Types 中不含参数
Tuple<int> t1; // Types 中包含一个参数:int
Tuple<int, float> t2; // Types 中包含两个参数:int 和 float
template<typename... Types> void f(Types... args);
f(); // args 中不包含参数
f(1); // args 中包含一个参数:int(1)
f(2, 1.0); // args 中包含两个参数:int(2) 和 double(1.0)
上面的两个示例中,Types
称作模板参数包,args
称作函数参数包。
参数包所包含的参数的个数可以用 sizeof...
取得。
Pack Expansion
既然提出了参数包,把所有可变参数容纳其中,那么就需要存在将其解包的操作。与 C 中 va_list
一个参数一个参数地手动解包不同,参数包的 Pack Expansion 是一口气将所有的参数以某种形式展开:
template<int... Entries>
struct IntArray {
int array[sizeof...(Entries)] = { Entries... };
};
template<typename... Types> void bar(Types... args) {}
template<typename... Types> void foo(Types... args) {
bar(&args...);
}
所谓「以某种形式」展开,就是将 pattern ...
转换为逗号分隔的 pattern_1, pattern_2, ... , pattern_N
的形式。从上面的函数 foo
就可以看出,传给 bar
的是各个参数的地址(没啥大意义);即 void foo(a, b, c)
的 &args...
会展开成 &a, &b, &c
。
std::tuple
作为一个可变参数模板的实际用例,C++11 还引入了 std::tuple
作为 std::pair
的推广形式(tuple 的意思即为元祖……哦不对,是元组……我是吃货我自重……),表示任意多个元素的组合。
使用 std::make_tuple
和 auto
可以很方便地声明一个元组:
auto x = std::make_tuple(3, 0.14, std::string("pie")); // std::tuple<int, double, std::string>
而对于各个元素的访问可以统一使用 std::get
实现(包括 std::array
和 std::pair
的大一统):
auto element = std::get<2>(x);
另一个好玩的地方是使用 std::tie
创建 lvalue reference 的 tuple:
std::set<int> some_instance_of_std_set;
std::set<int>::iterator itr;
bool success;
std::tie(itr, success) = some_instance_of_std_set.insert(2012);
虽然看着有些丑陋,但似乎可以看到些「多返回值」的影子……
当然,也可以参照 Lua 中的 _
使用 std::ignore
忽略多返回值中的特定位置的值:
int r1, r2;
std::tie(r1, std::ignore, r2) = std::make_tuple(3, 0.14, 4);
顺带的,既然是 lvalue reference,试图一句话交换两个变量的值是不可以全用 std::tie
的:
std::tie(a, b) = std::tie(b, a); // 错误方式
std::tie(a, b) = std::make_tuple(b, a); // 正确方式