前置:std::tuple

C++11引入了一个模板类型std::tuple(元组)。元组是一个固定大小的不同类型异质值的集合,也即它可以同时存放不同类型的数据。类似于python中用小括号表示的元组类型。C++已有的std::pair类型类似于一个二元组,可看作是std::tuple的一个特例,std::tuple也可看作是std::pair的泛化。std::pair的长度限制为2,而std::tuple的元素个数为0~任意个。

元组的使用

如果希望将一些数据组合成单一对象,但又不想麻烦地定义一个新数据结构来表示这些数据时,可以使用std::tuple。

可以将std::tuple看作一个“快速而随意”的数据结构,把它当作一个通用的结构体使用,但又不需要创建和获取结构体的特征,使得程序更加简洁。

创建一个std::tuple对象时,可以使用tuple的默认构造函数,它会对每个成员进行值初始化;也可以为每个成员提供一个初始值,此时的构造函数是explicit的,因此必须使用直接初始化方法。

使用get获取元组成员。为了使用get,必须指定一个显式模板实参,指出想要访问第几个成员。

传递给get一个tuple对象,返回指定成员的引用。get尖括号中的值必须是一个整型常量表达式。从0开始计数,get<0>是第一个成员。

为了使用tuple_size或tuple_element,需要知道一个元组对象的类型。确定一个对象的类型的最简单方法就是使用decltype。

std::tuple的一个常见用途是从一个函数返回多个值std::tuple中元素被紧密地存储的(位于连续的内存区域),而不是链式结构。std::tuple实现了多元组,这是一个编译期就确定大小的容器,可以容纳不同类型的元素。多元组类型在当前标准库中被定义为可以用任意数量参数初始化的类模板。每一模板参数确定多元组中一元素的类型。所以,多元组是一个多类型、大小固定的值的集合。

创建和初始化

std::tuple<int, double, std::string> first;
//创建一个空的元组,需要指定元组元素的数据类型,调用各个成员的默认构造函数进行初始化。
std::tuple<int, double, std::string> second(first);  
//拷贝构造
std::tuple<int, char> third(10, 'a');        
//创建并初始化,使用小括号初始化
std::tuple<int, std::string, double> fourth{42, "Test", -3.14};
// 创建并初始化,使用新的大括号初始化列表方式初始化
std::tuple<int, char> fifth(std::make_tuple(20, 'b'));
//移动构造,使用模板库的make_tuple
first = std::make_tuple(1, 3.14, "tuple"); //移动赋值
int i_third = 3;
std::tuple<int&> sixth(std::ref(i_third));//创建一个元组,元组的元素可以被引用

元组的访问和修改:std::get<N>()

int n=1;
auto t=std::make_tuple(10, "Test", 3.14, std::ref(n), n);
//get尖括号中的值必须是一个整型常量表达式。从0开始计数,意味着get<0>是第一个成员。
std::cout<<"The value of t is "<< "(" << 
std::get<0>(t) << ", " << std::get<1>(t) << ", "<< 
std::get<2>(t) << ", " << std::get<3>(t) << ", "<<
std::get<4>(t) << ")\n";
std::get<3>(t) = 9;
std::cout << n << std::endl;

元组的元素个数:使用std::tuple_size<>()

std::tuple<char,int,long,std::string> first('A',2,3,"4");
int i_count = std::tuple_size<decltype(first)>::value;
// 使用std::tuple_size计算元组个数
std::cout<<"the number of elements of a tuple:"<<i_count<<"\n";

元组的解包

std::tie() 元组包含一个或者多个元素,使用std::tie解包。首先需要定义对应元素的变量,再使用tie。

{ // std::tie: function template, Constructs a tuple object whose elements are references
// to the arguments in args, in the same order
//std::ignore: object, This object ignores any value assigned to it. It is designed to be used as an
  // argument for tie to indicate that a specific element in a tuple should be ignored.
    int myint;
    char mychar;
    std::tuple<int, float, char> mytuple;
    mytuple = std::make_tuple(10, 2.6, 'a');          // packing values into tuple
    std::tie(myint, std::ignore, mychar) = mytuple;   // unpacking tuple into variables 
    std::cout << "myint contains: " << myint << '\n';
    std::cout << "mychar contains: " << mychar << '\n';
}

元组的元素类型获取

获取元组中某个元素的数据类型,需要用到 std::tuple_element。

语法:std::tuple_element<index, tuple>

std::tuple<int, std::string> third(9, std::string("ABC"));
// 得到元组第1个元素的类型,用元组第一个元素的类型声明一个变量
std::tuple_element<1, decltype(third)>::type val_1;
// 获取元组的第一个元素的值
val_1 = std::get<1>(third);
std::cout << "val_1 = " << val_1.c_str() << "\n";

元组的拼接

使用 std::tuple_cat 执行拼接

std::tuple<char, int, double> first('A', 1, 2.2f);
//组合到一起,使用auto,自动推导
auto second = std::tuple_cat(first, std::make_tuple('B', std::string("-=+")));
//组合到一起,可以知道每一个元素的数据类型时什么 与 auto推导效果一样
std::tuple<char, int, double, char, std::string>third = 
std::tuple_cat(first, std::make_tuple('B', std::string("-=+")));

元组的遍历

元组没用提供operator []重载,遍历起来较为麻烦,需要为其单独提供遍历模板函数

template<class T>
void print_single(T const& v){
   if constexpr (std::is_same_v<T, std::decay_t<std::string>>)
        std::cout << std::quoted(v);
    else if constexpr (std::is_same_v<std::decay_t<T>, char>)
        std::cout << "'" << v << "'";
    else
        std::cout << v;
}
//helper function to print a tuple of any size
template<class Tuple, std::size_t N>
struct TuplePrinter {
    static void print(const Tuple& t)
    {
        TuplePrinter<Tuple, N-1>::print(t);
        std::cout << ", ";
        print_single(std::get<N-1>(t));
    }
};
template<class Tuple>
struct TuplePrinter<Tuple, 1>{
    static void print(const Tuple& t){
        print_single(std::get<0>(t));
    }
};
template<class... Args>
void print(const std::tuple<Args...>& t){
    std::cout << "(";
    TuplePrinter<decltype(t), sizeof...(Args)>::print(t);
    std::cout << ")\n";
}
// end helper function

优先使用枚举类而不是枚举类型:

(1)减少命名空间污染

枚举类(enum class)的枚举名仅在其枚举类型内可见,避免了命名冲突,减少了命名空间污染。枚举(enum)类型的枚举名会泄漏到其所在的作用域,可能导致命名冲突。

enum Color { black, white, red };   // black, white, red 在 Color 所在的作用域
auto white = false;                 // 错误! white 已经在这个作用域中声明
std::vector<std::size_t> primeFactors(std::size_t x); // func 返回 x 的质因子
Color c = red;
if (c < 14.5) {                     // Color 与 double 比较 (!)
    auto factors = primeFactors(c); // 计算一个 Color 的质因子 (!)
}

enum class Color { black, white, red }; // black, white, red 限制在 Color 域内
auto white = false;                 // 没问题,域内没有其他 “white”
Color c = Color::red;               //没问题
if (c < 14.5) {                     //错误.不能比较Color和double
}
if (static_cast<double>(c) < 14.5) { // 奇怪的代码,但是有效
    auto factors = primeFactors(static_cast<std::size_t>(c)); // 有问题,但是能通过编译
}

(2)强类型检查

限域枚举是强类型的,不允许隐式转换为整型或其他类型,从而避免了潜在的错误。例如,比较一个限域枚举和一个 double 会导致编译错误,而未限域枚举则会隐式转换为整型,可能导致意外的行为。

(3)前置声明

限域枚举可以被前置声明,有助于减少编译依赖。未限域枚举在 C++98 中不能被前置声明,但在 C++11 中可以通过指定底层类型来实现前置声明。

前置声明

enum class Status;    //前置声明
void continueProcessing(Status s); //使用前置声明 enum
// 枚举定义
enum class Status: std::uint32_t { good = 0,
failed = 1,incomplete = 100,
corrupt = 200,audited = 500,
indeterminate = 0xFFFFFFFF
 };

在某些情况下,使用未限域枚举可以更方便地与 std::tuple 配合使用,因为枚举名可以隐式转换为 std::size_t。然而,使用限域枚举需要额外的类型转换函数 toUType,虽然代码量增加,但可以避免命名空间污染和隐式类型转换带来的风险。

使用非限域枚举与 std::tuple

using UserInfo = std::tuple<std::string, std::string, std::size_t>;
enum UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
auto val = std::get<uiEmail>(uInfo); // 获取用户 email 字段的值

使用限域枚举与 std::tuple

enum class UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
template<typename E>
constexpr std::underlying_type_t<E> toUType(E enumerator) noexcept {
    return static_cast<std::underlying_type_t<E>>(enumerator);
}
auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);

作者建议在大多数情况下优先使用限域枚举,因为它们提供了更好的类型安全性和更清晰的命名空间管理。只有在特定场景下,如与 std::tuple 配合使用时,未限域枚举可能更加方便。