跳到主要内容C++17 标准库:std::any 类型擦除容器详解 | 极客日志C++
C++17 标准库:std::any 类型擦除容器详解
std::any 是 C++17 引入的类型擦除容器,用于在运行期存放任意类型的值。文章介绍了其构造方式(空、拷贝、移动、直接赋值、就地构造),特别强调了 constexpr 默认构造函数对避免静态初始化顺序问题的意义。内容涵盖主要成员函数如 emplace、reset、swap、has_value、type,以及非成员函数 any_cast 和 make_any 的使用方法和异常处理 bad_any_cast。通过代码示例展示了 lambda 表达式存储时的类型匹配问题及解决方案。
雾岛听风1 浏览 std::any 是 C++17 引入的一个'类型擦除(type erasure)'容器,用来 在运行期存放'任意类型的一个值',并在需要时再把它安全地取出来。
一个'有类型安全的 void* + RAII + RTTI'
构造
- 空 / 拷贝 / 移动
- '直接塞一个值进去'
- '就地构造(in-place)'
constexpr any() noexcept;
any( const any& other );
any( any&& other ) noexcept;
template< class ValueType >
any( ValueType&& value );
template< class ValueType, class... Args >
explicit any( std::in_place_type_t<ValueType>, Args&&... args );
template< class ValueType, class U, class... Args >
explicit any( std::in_place_type_t<ValueType>, std::initializer_list<U> il, Args&&... args );
示例
std::any a = "10";
std::any b = std::string("10");
std::any c(std::in_place_type<std::string>, 10, 'x');
in_place_type_t 是什么?
#include <utility>
template<class T>
struct in_place_type_t { };
template<class T>
inline constexpr in_place_type_t<T> in_place_type{};
| 名字 | 本质 |
|---|
std::in_place_type_t<T> | 一个类型 |
std::in_place_type<T> | 这个类型的一个 constexpr 对象 |
使用的时候一般都写 std::in_place_type<T>。
constexpr 默认构造函数的意义?
因为 std::any 的默认构造是 constexpr,所以'全局 / 静态的 std::any 对象'一定会在程序真正开始运行前就被安全地构造完成,不会踩到'静态初始化顺序问题'。
静态非局部对象与初始化顺序
一、什么是「静态非局部对象」?
struct X {
static std::any a;
};
这些都叫:static storage duration(静态存储期) 且是 non-local(非局部)。
二、静态对象初始化分两种(这是关键)
1️⃣ 静态初始化
- 编译期 / 装载期完成
- 不依赖运行时代码
- 顺序确定
- 100% 安全
2️⃣ 动态初始化
- 程序运行时执行
- 跨编译单元顺序不确定
- 经典问题:
👉 static initialization order fiasco
三、那 constexpr 默认构造有什么特殊?
std::any 的默认构造是:constexpr。它可以在编译期完成初始化,即它是静态初始化的。
四、对比一个'危险'的例子
❌ 经典的静态初始化顺序问题
std::string g_str = "hello";
extern std::string g_str;
struct X {
X() {
std::cout << g_str << std::endl;
}
};
X x;
⚠️ g_str 是 动态初始化
⚠️ 跨编译单元顺序未定义
⚠️ UB
extern std::any g_any;
struct X {
X() {
if (g_any.has_value()) {
}
}
};
X x;
g_any 在程序加载阶段就已经完成构造
- 至少是一个'空 any'
- 不会访问未初始化内存
五、为什么标准要特意强调这一点?
因为 std::any 是一个'类型擦除容器',你可能会担心:
- 会不会内部有堆分配?
- 会不会依赖 RTTI 初始化?
- 会不会像
std::string 一样有动态初始化?
默认构造的 std::any 非常'轻',可以放心作为全局对象使用
成员函数:修改器
std::any::emplace
template< class ValueType, class... Args >
std::decay_t<ValueType>& emplace( Args&&... args );
template< class ValueType, class U, class... Args >
std::decay_t<ValueType>& emplace( std::initializer_list<U> il, Args&&... args );
更改所含对象为从参数构造的 std::decay_t<ValueType> 类型对象。
注意首先要用 reset() 销毁当前所含对象(若存在)。
模板形参
返回值
异常
抛出 T 构造函数所抛的任何异常。若抛出异常,则销毁先前所含对象(若存在),而 *this 不含值。
示例
#include <algorithm>
#include <any>
#include <iostream>
#include <string>
#include <vector>
class Star {
std::string name;
int id;
public:
Star(std::string name, int id) : name { name }, id { id } {
std::cout << "Star::Star(string, int)\n";
}
void print() const {
std::cout << "Star{ \"" << name << "\" : " << id << " };\n";
}
};
auto main() -> int {
std::any celestial;
celestial.emplace<Star>("Procyon", 2943);
const auto* star = std::any_cast<Star>(&celestial);
star->print();
std::any av;
av.emplace<std::vector<char>>({ 'C', '+', '+', '1', '7' });
std::cout << av.type().name() << '\n';
const auto* va = std::any_cast<std::vector<char>>(&av);
std::for_each(va->cbegin(), va->cend(), [](char const& c) {
std::cout << c;
});
std::cout << '\n';
}
Star::Star(string, int)
Star{ "Procyon" : 2943 };
St6vectorIcSaIcEE
C++17
std::any::reset
std::any::swap
void swap(any& other) noexcept;
成员函数:观察器
std::any::has_value
bool has_value() const noexcept;
检查对象是否含有值。若实例含值则为 true,否则为 false。
示例
#include <any>
#include <iostream>
#include <string>
int main() {
std::boolalpha(std::cout);
std::any a0;
std::cout << "a0.has_value(): " << a0.has_value() << "\n";
std::any a1 = 42;
std::cout << "a1.has_value(): " << a1.has_value() << '\n';
std::cout << "a1 = " << std::any_cast<int>(a1) << '\n';
a1.reset();
std::cout << "a1.has_value(): " << a1.has_value() << '\n';
auto a2 = std::make_any<std::string>("Milky Way");
std::cout << "a2.has_value(): " << a2.has_value() << '\n';
std::cout << "a2 = \"" << std::any_cast<std::string&>(a2) << "\"\n";
a2.reset();
std::cout << "a2.has_value(): " << a2.has_value() << '\n';
}
a0.has_value(): false
a1.has_value(): true
a1 = 42
a1.has_value(): false
a2.has_value(): true
a2 = "Milky Way"
a2.has_value(): false
std::any::type
const std::type_info& type() const noexcept;
查询所含类型。若实例非空则为所含值的 typeid,否则为 typeid(void)。
非成员函数
std::any_cast
(1) template<class T> T any_cast(const any& operand);
(2) template<class T> T any_cast(any& operand);
(3) template<class T> T any_cast(any&& operand);
(4) template<class T> const T* any_cast(const any* operand) noexcept;
(5) template<class T> T* any_cast(any* operand) noexcept;
令 U 为 std::remove_cv_t<std::remove_reference_t<T>>。(将 T 类型移除引用和 const)
参数
返回值
1-2) 返回 static_cast<T>(*std::any_cast<U>(&operand))。
- 返回
static_cast<T>(std::move(*std::any_cast<U>(&operand)))。
4-5) 若 operand 不是空指针,且请求的 T 的 typeid 匹配 operand 的 typeid,则为指向所含值的指针,否则为空指针。
异常
1-3) 若请求的 T 的 typeid 不匹配 operand 内容的 typeid,则抛出 std::bad_any_cast。
示例
#include <string>
#include <iostream>
#include <any>
#include <utility>
int main() {
auto a = std::any(12);
std::cout << std::any_cast<int>(a) << '\n';
try {
std::cout << std::any_cast<std::string>(a) << '\n';
} catch(const std::bad_any_cast& e) {
std::cout << e.what() << '\n';
}
if (int* i = std::any_cast<int>(&a)) {
std::cout << "a is int: " << *i << '\n';
} else if (std::string* s = std::any_cast<std::string>(&a)) {
std::cout << "a is std::string: " << *s << '\n';
} else {
std::cout << "a is another type or unset\n";
}
a = std::string("hello");
auto& ra = std::any_cast<std::string&>(a);
ra[1] = 'o';
std::cout << "a: " << std::any_cast<const std::string&>(a) << '\n';
auto b = std::any_cast<std::string&&>(std::move(a));
std::cout << "a: " << *std::any_cast<std::string>(&a) << "\n";
std::cout << "b: " << b << '\n';
}
12
bad any_cast
a is int: 12
a: hollo
a:
b: hollo
std::make_any
构造含 T 类型对象的 any 对象,传递提供的参数给 T 的构造函数。
-
等价于 return std::any(std::in_place_type<T>, std::forward<Args>(args)...);
-
等价于 return std::any(std::in_place_type<T>, il, std::forward<Args>(args)...);
示例
#include <any>
#include <complex>
#include <functional>
#include <iostream>
#include <string>
int main() {
auto a0 = std::make_any<std::string>("Hello, std::any!\n");
auto a1 = std::make_any<std::complex<double>>(0.1, 2.3);
std::cout << std::any_cast<std::string&>(a0);
std::cout << std::any_cast<std::complex<double>&>(a1) << '\n';
using lambda = std::function<void(void)>;
std::any a2 = [] { std::cout << "Lambda #1.\n"; };
std::cout << "a2.type() = \"" << a2.type().name() << "\"\n";
try {
std::any_cast<lambda>(a2)();
} catch (std::bad_any_cast const& ex) {
std::cout << ex.what() << '\n';
}
auto a3 = std::make_any<lambda>([] { std::cout << "Lambda #2.\n"; });
std::cout << "a3.type() = \"" << a3.type().name() << "\"\n";
std::any_cast<lambda>(a3)();
}
Hello, std::any!
(0.1,2.3)
a2.type() = "Z4mainEUlvE_"
bad any_cast
a3.type() = "St8functionIFvvEE"
Lambda
为什么#1会抛异常呢,因为 lambda 表达式的类型不等于 std::function,它是一个编译器生成的、匿名的、唯一的类型:
struct <unique_lambda_type> {
void operator()() const;
};
any_cast<lambda> 中需要计算类型 T 和 U:
T = std::function<void(void)>
U = remove_cvref_t<T> = std::function<void(void)>
typeid(<unique_lambda_type>) == typeid(std::function<void(void)>) ?
辅助类
std::bad_any_cast
class bad_any_cast : public std::bad_cast;
定义 std::any_cast 在失败时以值返回形式抛出的对象的类型。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online