— 本文为学习笔记,如有建议欢迎指出~
一、std::tuple
C++ tuple 是一个功能强大的工具,它提供了灵活的方式来组合不同类型的数据。随着 C++17 的结构化绑定,tuple 的使用变得更加简洁。在需要返回多个值或者临时组合数据时,tuple 是一个很好的选择。但在性能敏感的场景中,要注意 tuple 可能带来的拷贝开销(可以通过引用或移动语义优化)。
定义:是 C++11 引入的标准库组件,用于存储固定大小的异构数据集合(可包含不同类型元素)。
1. 头文件
#include <tuple>
2. 创建tuple
法一:使用std::tuple
法二:使用std::make_tuple函数模板(自动推导)
auto t1 = std::make_tuple(1,3.14,"hello"); //自动推导
C++17起,可以使用类模板参数推导:
std::tuple t2(1,3.14,"hello"); //自动推导为tuple<int,double,const char*>
法三:使用初始化列表
std::tuple<int,double,std::string> t1(1,3.14,"hello") //直接构造
3. 访问tuple(std::get)
不能直接用下标访问,应使用 std::get 函数模板,有两种方式指定元素:
- 通过索引(编译器常量);
- 通过类型(如果类型在tuple中唯一);
auto t = std::make_tuple(1,3.14,"hello");
//通过索引访问
int i = std::get<0>(t); //获取第一个元素,索引0
double d = std::get<1>(t); //第二个元素
std::string s = std::get<2>(t); //第三个元素
//通过类型访问
int j = std::get<int>(t); //获取int类型的元素
double e = std::get<double>(t); //获取double类型的元素
4. 获取tuple大小(std::tuple_size)
使用std::tuple_size 模板类(需要类型,而不是对象)来获取元素个数
auto t = std::make_tuple(1,3.14,"hello");
size_t_size = std::tuple_size<decltype(t)>::value; C++11 / 14
//使用C++17变量模板
size_t_size2 = std::tuple_size_v<decltype(t)>;
注:decltype 关键字
用于在编译时推导表达式的类型,不会实际计算表达式,只进行静态类型分析
int x = 20;
decltype(x) y = 10; //y的类型与x相同为int
5. 获取tuple中元素的类型(std::tuple_element)
使用std::tuple_element模板类可获取tuple中某个位置元素的类型
std::tuple<int,double,std::string> t;
using first_type = std::tuple_element<0,decltype(t)>::type;
//获取了第0个元素的类型 int
6. 解包tuple(std::tie、std:ignore)
使用std::tie将tuple的元素解包到变量中。注:std:die创建的是一个引用的tuple,所以可以用来修改原tuple(若原tuple是非const的)
auto t = std::make_tuple(1, 3.14, "hello");
int a;
double b;
std::string c;
std::tie(a, b, c) = t; // 将t中的元素分别赋值给a, b, c
// 也可以使用std::ignore忽略某些元素
std::tie(a, b, std::ignore) = t; // 忽略第三个元素
// 注意:C++17 引入了结构化绑定(更简洁)
auto [x, y, z] = t; // 直接声明x,y,z并赋值,类型自动推导
7. 比较tuple
tuple 支持比较运算符(==, !=, <, <=, >, >=),按字典序比较(逐个元素比较,直到能确定大小关系)。
std::tuple<int, int> t1(1, 2);
std::tuple<int, int> t2(1, 3);
if (t1 < t2)
{
// true,因为第一个元素相等,第二个元素2<3
// ...
}
8. 链接tuple(std::tuple_cat)
可以使用 `std::tuple_cat` 将多个 tuple 连接成一个新的 tuple。
std::tuple<int, int> t1(1, 2);
std::tuple<double, char> t2(3.14, 'a');
auto t3 = std::tuple_cat(t1, t2); // t3 是 tuple<int, int, double, char>
9. 交换tuple(std::swap)
使用 `std::swap` 可以交换两个相同类型的 tuple。
std::tuple<int, char> t1(1, 'a');
std::tuple<int, char> t2(2, 'b');
std::swap(t1, t2); // 交换内容
10.引用(std::ref、std::cref)
tuple 可以包含引用,使用 `std::ref` 和 `std::cref` 来创建引用和常引用。
int a = 1;
double b = 2.0;
auto t = std::make_tuple(std::ref(a), std::cref(b));
std::get<0>(t) = 10; // 修改a的值,a变成10
// std::get<1>(t) = 3.0; // 错误,因为是const引用
11. 注意事项
- 存储:tuple 的元素在内存中是连续存储的(通常按照声明顺序),但可能由于对齐而有填充。
- 常用场景:tuple 常用于函数返回多个值,特别是当这些值类型不同时。
- 访问开销:std::get<>是编译时计算,无运行时开销
- 内存占用:通常等于各个元素大小总和(加对齐填充)
- 拷贝成本:元素逐个拷贝(大对象考虑移动语义)
对大型对象使用 std::make_tuple + std::ref:
std::vector<int> large_vec;
auto t = std::make_tuple(std::ref(large_vec)); // 避免拷贝
12. C++17 结构化绑定
auto get_values()
{
return std::make_tuple(10, 20.5, "hello");
}
int main()
{
auto [a, b, c] = get_values(); // 直接解包
// a是int, b是double, c是const char*(或std::string,取决于返回类型)
auto& [x, y, z] = t1; // 引用绑定
//需要修改元组元素时用 auto&
}
13.使用方式
- 存储引用
int val = 100;
std::tuple<int&> t_ref(val); // 存储引用
std::get<0>(t_ref) = 200; // val 被修改为200
- 替代多返回值
std::tuple<int, std::string> GetData()
{
return {404, "Not Found"};
}
auto [code, msg] = GetData(); // code=404, msg="Not Found"
- 类型萃取
using T = std::tuple_element<1, decltype(t1)>::type; // T = double
static_assert(std::is_same_v<T, double>);
14. 使用场景
- 函数返回多个值
- 代替结构体,当结构体只使用一次且不想定义时
- 需要将不同类型的数据组合在一起,但又不想定义新类型
- 在模板编程中,处理可变类型列表
二、结构体
结构体在C++中是一个功能全面的自定义数据类型,它与类几乎相同,只是默认访问权限不同。结构体适合用于数据聚合的场景,例如点、矩形、颜色等轻量级数据结构。
随着C++标准的更新,结构体的功能也在不断增强,如支持构造函数、继承等特性。
注意:在C++中,结构体可以拥有几乎所有类能拥有的特性(如成员函数、静态成员、继承、多态等),因此选择使用结构体还是类通常取决于设计意图:如果主要是数据聚合,使用结构体;如果需要封装和复杂行为,使用类。但这不是强制规则,可根据习惯选择。
1. 基本定义
结构体通过struct关键字定义,可以包含多个不同类型的数据成员(变量)和成员函数
struct Point
{
// 声明结构体
double x; // 成员变量(默认public)
double y;
void print()
{
// 成员函数
std::cout << "(" << x << ", " << y << ")";
}
};
2. 创建结构体对象
Point p1; // 默认初始化,成员值未定义(如果是内置类型)
Point p2 = {1.0, 2.0}; // 聚合初始化(C++11起)
Point p3{3.0, 4.0}; // 直接初始化(C++11)
p1.x = 5.0; p1.y = 6.0; // 单独赋值
3. 访问结构体成员
使用点操作符(`.`)访问成员
p1.print(); // 调用成员函数
std::cout << p2.x << ", " << p2.y << std::endl;
4. 结构体与类的区别
struct 和 class 的唯一区别是默认访问权限,但可以显式指定访问权限,因此两者功能完全等价,选择取决于编程风格。
- struct:默认成员为public
- class:默认成员为private
此外,继承时的默认访问权限:
- struct 继承默认是 public
- class 继承默认是 private
5. 结构体的构造函数
可以为结构体定义构造函数,包括默认构造函数、参数化构造函数等。
struct Point
{
double x, y;
// 默认构造函数(如果定义了其他构造函数,编译器不再生成默认构造)
Point() : x(0.0), y(0.0) {}
// 参数化构造函数
Point(double a, double b) : x(a), y(b) {}
// 委托构造函数(C++11)
Point(double a) : Point(a, 0.0) {}
};
6. 结构体的聚合初始化
如果结构体满足以下条件,则是一个聚合类型(Aggregate):
- 所有成员都是public
- 没有用户提供的构造函数(C++11之前,C++14放宽了条件)
- 没有基类和虚函数
- 没有默认成员初始化器(C++11之前,C++14允许)
聚合类型可以使用花括号初始化:
Point p = {1.0, 2.0};
7. 结构体中的静态成员
结构体可以有静态成员变量和静态成员函数
struct Widget
{
static int count; // 静态成员变量声明
Widget()
{
count++;
}
static void printCount()
{
std::cout << count;
}
};
int Widget::count = 0; // 静态成员变量定义
8. 结构体嵌套
结构体可以嵌套定义
struct Line
{
struct Point
{
double x, y;
}
start, end;
};
// 使用
Line l;
l.start.x = 0.0;
9. 结构体与函数
结构体可以作为函数参数和返回值
Point add(Point a, Point b)
{
return {a.x+b.x, a.y+b.y};
}
10. 结构体的大小与内存对齐
结构体的大小受内存对齐影响。可以使用sizeof获取大小,alignof获取对齐要求
struct Data {
char c; // 1字节
int i; // 4字节(通常有3字节填充)
double d; // 8字节
};
// sizeof(Data) 通常为 16 (1+3+4+8)
显示控制对齐
struct alignas(16) AlignedData {
float arr[4];
};
11. 结构体的位域
可以定义成员占用特定位数。
struct Status {
unsigned int flag1 : 1; // 1位
unsigned int flag2 : 2; // 2位
};
12. 匿名结构体(C++11扩展)
匿名结构体(通常用于联合体union中)可以没有名字。
union Value {
struct
{
int x, y;
}; // 匿名结构体
int data[2];
};
Value v;
v.x = 10; // 直接访问
13. 结构体的继承(C++11)
结构体可以继承其他结构体或类(因为结构体也是类)。
struct Base
{
int base_data;
};
struct Derived : Base
{
int derived_data;
};
14. 结构体的类型别名
可以使用`typedef`或`using`为结构体创建别名。
typedef Point MyPoint;
using MyPoint2 = Point;
15. 结构体与模板
结构体可以是模板类。
template <typename T>
struct Box {
T contents;
};
Box<int> intBox{42};
16. 结构体的反射(C++20有限支持)
C++20引入的反射特性(尚未完全支持)可以操作结构体的成员。
三、二者区别和使用场景
1. 二者区别
2. 使用场景
① 优先使用元组的场景:
- 临时数据组合
// 函数返回多个临时值
auto getCoordinates() {
return std::make_tuple(10.5, 20.3);
}
auto [x, y] = getCoordinates();
- 泛型编程
// 处理任意类型组合
template <typename... Ts>
void processItems(const std::tuple<Ts...>& items) {
// ...
}
- 编译时类型操作
// 元编程中提取类型
using SecondType = std::tuple_element_t<1, MyTuple>;
- 快速原型开发
// 临时测试避免定义结构体
auto userData = std::make_tuple("Alice", 30, 85.5);
② 优先使用结构体的场景:
- 业务实体建模
// 明确语义的数据对象
struct Employee {
std::string name;
int id;
double salary;
void print() const { ... }
};
- 内存敏感场景
// 精确控制内存布局
#pragma pack(push, 1)
struct SensorData {
uint32_t timestamp;
float values[3];
uint8_t status : 4;
};
#pragma pack(pop)
- 需要行为扩展
struct Vector3D {
float x, y, z;
Vector3D operator+(const Vector3D& other) const {
return {x+other.x, y+other.y, z+other.z};
}
};
- 接口设计/API边界
// 清晰的API参数
void drawRectangle(const Rectangle& rect); // 优于 tuple<int,int,int,int>
- 持久化/序列化
struct UserProfile {
std::string username;
std::hash<std::string> password_hash;
time_t registration_date;
// 可添加序列化方法
std::string serialize() const;
};
2. 转换与操作
- 元组 → 结构体(C++ 17)
auto t = std::make_tuple("Bob", 25);
struct Person { std::string name; int age; };
auto [name, age] = t;
Person p{name, age}; // 显式转换
- 结构体 → 元组
Point p{1.5, 2.5};
auto t = std::make_tuple(p.x, p.y); // 手动解构
// 或使用tie创建引用元组
float x, y;
std::tie(x, y) = p; // 绑定到变量
3. 黄金准则
- 可维护性优先:当字段超过三个或需长期维护时,总是选择结构体
- 性能关键区:内存敏感场景(如嵌入式/高频交易)优先使用结构体+手动内存控制
- 模板元编程:类型操作/可变参数处理时元组更适合
- API设计:公共接口暴露时,永远选择结构体(提高可读性和稳定性)
- C++17+新项目:充分利用结构化绑定简化二者使用:
// 统一访问方式 auto [a, b] = getTuple(); // 元组 auto [x, y] = Point{1,2}; // 结构体