C++17完整导引-新组件之std::any

std::any

一般来说,C++是一门类型绑定和类型安全的语言。**值对象被声明为确定的类型,**这个类型定义了所有可能的操作、也定义了对象的行为。而且,对象不能改变自身的类型。

std::any一种在保证类型安全的基础上还能改变自身类型的值类型。也就是说,**它可以持有任意类型的值,并且它知道自己当前持有的值是什么类型的。**当声明一个这种类型的对象时不需要指明所有可能的类型。通俗点讲就是,可以检查是否存储了值,并且可以安全地检索和转换存储的值。

实现的关键在于std::any对象同时包含了值和值的类型。因为内含的值可以有任意的大小,所以可能会在堆上分配内存。然而,实现应该尽量避免为小类型的值例如int在堆上分配内存。

对于一个std::any对象,如果你赋值为一个字符串,它将会分配内存并拷贝字符串,并存储记录当前的值是一个字符串。之后,可以使用运行时检查来判断当前的值的类型。为了将当前的值转换为真实的类型,必须要使用any_cast<>

std::optional<>、std::variant<>一样,std::any对象有值语义。也就是说,拷贝被实现为 深拷贝 ,会创建一个在自己的内存中持有当前值的独立对象。因为可能会使用堆内存,所以拷贝std::any开销一般都很大。更推荐以引用传递对象,或者move值。std::any支持部分move语义。

使用std::any

下面示例演示了std::any的核心能力:

#include <any>
#include <iostream>
#include <string>

using namespace std;

void judgeTypePrint(const std::any &a) {
    if (a.type() == typeid(std::string)) {
        std::string s = std::any_cast<std::string>(a);
        cout << "using string " << s << endl;
    } else if (a.type() == typeid(int)) {
        cout << "using int " << endl;
    }
}
int main() {
    std::any a;             // a为空
    std::any b = 4.3;       // b有类型为double的值4.3
    a = 42;                 // a有类型为int的值42
    b = std::string{"hi"};  // b有类型为std::string的值"hi"
    judgeTypePrint(a);
    judgeTypePrint(b);
}

输出如下:

using int 
using string hi

你可以声明一个空的std::any,也可以用某个类型的值初始化。如果传递了初始值,std::any内含的值的类型将变为初始值的类型。通过使用成员函数type(),你可以检查内含值的类型和某一个类型的ID是否相同。如果对象是空的,类型ID将等于typeid(void)
为了访问内部的值,你必须使用std::any_cast<>将它转换为真正的类型:

auto s = std::any_cast<std::string>(a);

如果转换失败,可能是因为对象为空或者与内部值的类型不匹配,这时会抛出一个std::bad_any_cast异常。因此,在不知道当前类型的情况下,你最好像下面这样使用:

try {
    auto s = std::any_cast<std::string>(a);
    ...
}
catch (std::bad_any_cast& e) {
    std::cerr << "EXCEPTION: " << e.what() << '\n';
}

预处理代码如下:

  std::any a = std::any();
  try 
  {
    std::basic_string<char> s = std::any_cast<std::basic_string<char> >(a);
  } catch(std::bad_any_cast & e) {
    std::operator<<(std::operator<<(std::operator<<(std::cerr, "EXCEPTION: "), e.what()), '\n');
  }

注意std::any_cast<>会创建一个指定类型的对象。例如这个例子中,如果你用std::string作为std::any_cast<>的模板参数,它将创建一个临时的字符串(prvalue),然后使用临时字符串初始化新的对象s如果不需要初始化其他变量,更推荐转换为引用类型来避免创建临时对象

std::cout << std::any_cast<const std::string&>(a);

如果想要修改当前的值,也需要转换为相应的引用类型:

std::any_cast<std::string&>(a) = "world";

你也可以对一个std::any对象的地址调用std::any_cast。这种情况下,如果类型匹配那么转换结果将会是一个相应类型的指针,否则将返回nullptr

#include <any>
#include <iostream>
#include <string>

using namespace std;

int main() {
    std::any a;             // a为空
    std::any b = 4.3;       // b有类型为double的值4.3
    a = 42;                 // a有类型为int的值42
    b = std::string{"hi"};  // b有类型为std::string的值"hi"

    auto p = std::any_cast<std::string>(&a);
    if (p) {
        cout << " any_cast ok " << endl;
    } else {
        cout << " any_cast failed " << endl;
    }

    auto p1 = std::any_cast<std::string>(&b);
    if (p1) {
        cout << " any_cast ok " << endl;
    } else {
        cout << " any_cast failed " << endl;
    }
}

预处理代码如下:

std::any a = std::any();
  std::any b = std::any(4.2999999999999998);
  a.operator=(42);
  b.operator=(std::basic_string<char, std::char_traits<char>, std::allocator<char> >{"hi", std::allocator<char>()});
  std::basic_string<char, std::char_traits<char>, std::allocator<char> > * p = std::any_cast<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(&a);
  if(p) {
    std::operator<<(std::cout, " any_cast ok ").operator<<(std::endl);
  } else {
    std::operator<<(std::cout, " any_cast failed ").operator<<(std::endl);
  } 
  
  std::basic_string<char, std::char_traits<char>, std::allocator<char> > * p1 = std::any_cast<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(&b);
  if(p1) {
    std::operator<<(std::cout, " any_cast ok ").operator<<(std::endl);
  } else {
    std::operator<<(std::cout, " any_cast failed ").operator<<(std::endl);
  } 

或者,可以使用新的带初始化的if语句:

if (auto p = std::any_cast<std::string>(&a); p != nullptr) {
    ...
}

或者:

if (auto p = std::any_cast<std::string>(&a)) {
    ...
}

为了清空一个std::any对象,你可以调用:

a.reset();   // 清空对象

或者:

a = std::any{};

或者:

a = {};

你也可以直接检查对象是否有值:

if (a.has_value()) {
    ...
}

注意存储值时类型会退化(数组转换为指针,顶层引用和const被忽略)。对于字符串字面量,值类型将是const char*。为了使用std::any_cast<>进行转换,你必须显式指明这个类型:

std::any a = "hello";   // type()是const char*
if (a.type() == typeid(const char*)) {  // true
    ...
}
if (a.type() == typeid(std::string)) {  // false
    ...
}
std::cout << std::any_cast<const char*>(a) << '\n'; // OK
std::cout << std::any_cast<std::string>(a) << '\n'; // EXCEPTION

这基本就是std::any支持的所有操作了。没有定义比较运算符(这意味着你不能比较或者排序对象)。没有定义哈希函数,也没有定义value()成员函数。而且,因为类型只有在运行时才能获取,所以也不能使用泛型lambda来独立于类型处理当前的值。你只能使用运行时的std::any_cast<>函数来处理当前的值,这意味着处理当前值时你需要一些类型特定的代码来重入C++的类型系统。

然而,你可以把std::any对象放置在容器中。例如:

std::vector<std::any> v;

v.push_back(42);
std::string s = "hello";
v.push_back(s);

for (const auto& a : v) {
    if (auto pa = std::any_cast<const std::string>(&a); pa != nullptr) {
        std::cout << "string: " << *pa << '\n';
    }
    else if (auto pa = std::any_cast<const int>(&a); pa != nullptr) {
        std::cout << "int: " << *pa << '\n';
    }
}

注意你应该总是使用这样的if-else链。这里不能使用switch语句。

类型和操作

类型

在头文件中,C++标准库以如下方式定义了类std::any

namespace std {
    class any;
}

也就是说,std::any根本就不是模板类
另外,还定义了下面的类型和对象:

  • 异常类std::bad_any_cast,**当转换失败时会抛出这种异常。**这个类派生自std::bad_cast,后者又派生自std::exceptionstd::any类也使用了定义在头文件<utility>中的std::in_place_type对象(类型是std::in_place_type_t)。如果我们试图从std::any对象中检索不正确的类型,std::bad_any_cast异常将被抛出。此外,如果std::any对象没有存储任何值,则std::bad_any_cast异常也会被抛出。因此,在使用std::any_cast函数时应谨慎,最好使用std::any::has_value函数检查std::any对象是否包含值。

操作

std::any的操作列出了std::any的所有操作:

操作效果
构造函数创建一个any对象(可能会调用底层类型的构造函数)
make_any<>()创建一个any对象(传递参数来初始化)
析构函数销毁any对象
=赋予新值
emplace<T>()赋予一个类型T的新值
reset()销毁any类型(使对象变为空)
has_value()返回对象是否持有值
type()std::type_info对象返回当前类型
any_cast<T>()将当前值转换为类型T的值(如果类型不正确将抛出异常/返回nullptr
swap()交换两个对象的值
构造函数

构造函数声明如下:

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被初始化为空。

std::any a1;        // a1是空的

如果传递值来初始化,内含值的类型将是它 退化 后的类型:

std::any a2 = 42;       // a2包含int类型的值
std::any a3 = "hello";  // a2包含const char*类型的值

为了使内部值的类型和初始值的类型不同,你需要使用in_place_type标记:

std::any a4{std::in_place_type<long>, 42};
std::any a5{std::in_place_type<std::string>, "hello"};

传给in_place_type的类型也可能退化。下面的代码声明了一个持有const char*的对象:

std::any a5b{std::in_place_type<const char[6]>, "hello"};

为了用多个参数初始化std::any对象,你必须手动创建对象或者你可以添加std::in_place_type作为第一个参数(因为内含类型不能直接从多个初始值推导出来):

std::any a6{std::complex{3.0, 4.0}};
std::any a7{std::in_place_type<std::complex<double>>, 3.0, 4.0};

预处理代码如下:

  std::any a6 = std::any{std::complex<double>{3.0, 4.0}};
  std::any a7 = std::any{std::in_place_type_t<std::complex<double> >(std::in_place_type<std::complex<double> >), 3.0, 4.0};

你甚至可以传递初值列和其他参数:

// 用一个以lambda为排序准则的set初始化std::any对象
    auto sc = [](int x, int y) { return std::abs(x) < std::abs(y); };
    std::any a8{std::in_place_type<std::set<int, decltype(sc)>>,
                {4, 8, -7, -2, 0, 5},
                sc};

    cout<< a8.has_value()<<endl;
    auto a = std::any_cast<std::set<int, decltype(sc)>>(a8);

预处理代码如下:

  class __lambda_12_15
  {
    public: 
    inline /*constexpr */ bool operator()(int x, int y) const
    {
      return abs(x) < abs(y);
    }
    
    using retType_12_15 = bool (*)(int, int);
    inline constexpr operator retType_12_15 () const noexcept
    {
      return __invoke;
    };
    
    private: 
    static inline /*constexpr */ bool __invoke(int x, int y)
    {
      return __lambda_12_15{}.operator()(x, y);
    }
    
    public: 
    // inline /*constexpr */ __lambda_12_15 & operator=(const __lambda_12_15 &) /* noexcept */ = delete;
    // inline /*constexpr */ __lambda_12_15(const __lambda_12_15 &) noexcept = default;
    
  };
  
  __lambda_12_15 sc = __lambda_12_15{};
  std::any a8 = std::any{std::in_place_type_t<std::set<int, __lambda_12_15, std::allocator<int> > >(std::in_place_type<std::set<int, __lambda_12_15, std::allocator<int> > >), std::initializer_list<int>{4, 8, -7, -2, 0, 5}, sc};
  std::cout.operator<<(a8.has_value()).operator<<(std::endl);
  std::set<int, __lambda_12_15, std::allocator<int> > a = std::any_cast<std::set<int, __lambda_12_15, std::allocator<int> > >(a8);

注意还有一个快捷函数make_any<>(),它可以接受一个或多个参数(不需要使用in_place_type参数)。你必须显式指明初始化的类型(即使只有一个参数它也不会自动推导类型):

auto a10 = std::make_any<float>(3.0);
auto a11 = std::make_any<std::string>("hello");
auto a13 = std::make_any<std::complex<double>>(3.0, 4.0);
auto a14 = std::make_any<std::set<int, decltype(sc)>> ({4, 8, -7, -2, 0, 5}, sc);

综合示例如下:

#include <any>
#include <boost/core/demangle.hpp> //需要添加boost 库
#include <initializer_list>
#include <iostream>
#include <memory>
#include <set>
#include <string>
#include <utility>

struct A {
    int age;
    std::string name;
    double salary;

#if __cpp_aggregate_paren_init < 201902L
    // Required before C++20 for in-place construction
    A(int age, std::string name, double salary)
        : age(age), name(std::move(name)), salary(salary) {}
#endif
};

//使用abi demangle打印任意持有的实例的良好类型名称
void printType(const std::any& a) {
    std::cout << boost::core::demangle(a.type().name()) << '\n';
}

int main() {
    std::any a1{7};
    std::any a2(std::in_place_type<A>, 30, "Ada", 1000.25);
    auto lambda = [](auto&& l, auto&& r) { return l.age < r.age; };
    std::any a3(
        std::in_place_type<std::set<A, decltype(lambda)>>,
        {A{39, std::string{"Ada"}, 100.25}, A{20, std::string{"Bob"}, 75.5}},
        lambda);

    printType(a1);
    printType(a2);
    printType(a3);
}

预处理代码如下:

 std::any a1 = std::any{7};
  std::any a2 = std::any(std::in_place_type_t<A>(std::in_place_type<A>), 30, "Ada", 1000.25);
    
  class __lambda_29_19
  {
    public: 
    template<class type_parameter_0_0, class type_parameter_0_1>
    inline /*constexpr */ auto operator()(type_parameter_0_0 && l, type_parameter_0_1 && r) const
    {
      return l.age < r.age;
    }
    
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline /*constexpr */ bool operator()<const A &, const A &>(const A & l, const A & r) const
    {
      return l.age < r.age;
    }
    #endif
    
    private: 
    template<class type_parameter_0_0, class type_parameter_0_1>
    static inline /*constexpr */ auto __invoke(type_parameter_0_0 && l, type_parameter_0_1 && r)
    {
      return __lambda_29_19{}.operator()<type_parameter_0_0, type_parameter_0_1>(l, r);
    }
    public: 
    // inline /*constexpr */ __lambda_29_19 & operator=(const __lambda_29_19 &) /* noexcept */ = delete;
    // inline /*constexpr */ __lambda_29_19(const __lambda_29_19 &) noexcept = default;
    
  };
  
  __lambda_29_19 lambda = __lambda_29_19{};
  std::any a3 = std::any(std::in_place_type_t<std::set<A, __lambda_29_19, std::allocator<A> > >(std::in_place_type<std::set<A, __lambda_29_19, std::allocator<A> > >), std::initializer_list<A>{A{39, std::basic_string<char>{"Ada", std::allocator<char>()}, 100.25}, A{20, std::basic_string<char>{"Bob", std::allocator<char>()}, 75.5}}, lambda);
 

运行结果如下:

int
A
std::set<A, main::{lambda(auto:1&&, auto:2&&)#1}, std::allocator<A> >
修改值

可以使用赋值操作和emplace()来修改值。例如:

std::any a;

a = 42;         // a含有int类型的值
a = "hello";    // a含有const char*类型的值
a.emplace<std::string>("hello");            // a含有std::string类型的值
a.emplace<std::complex<double>>(4.4, 5.5);  // a含有std::complex<double>类型的值
访问值

为了访问内含的值,你必须使用std::any_cast<>将它转换为真实的类型。例如为了把值转换成string,你有以下几种方法:

std::any_cast<std::string>(a);        // 返回值的拷贝
std::any_cast<std::string&>(a);       // 通过引用获取写权限
std::any_cast<const std::string&>(a); // 通过引用获取读权限

如果移除掉顶层引用和const之后类型ID相同则说明类型匹配。如果转换失败,将会抛出一个std::bad_any_cast异常。

为了避免异常处理,你可以传递any对象的地址。当因为类型不匹配导致转换失败时它会返回nullptr

if (auto sp{std::any_cast<std::string>(&a)}; sp != nullptr) {
    ... // 使用*sp获取a的值的写权限
}

if (auto sp{std::any_cast<const std::string>(&a)}; sp != nullptr) {
    ... // 使用*sp获取a的值的读权限
}

注意,这里如果转换为引用将导致运行时错误:

std::any_cast<std::string&>(&a);    // 运行时错误
move语义

std::any也支持move语义。然而,注意前提是底层类型要支持拷贝语义。也就是说 move-only类型 不支持作为内含的值类型。
处理move语义的最佳方式可能不是很明显,你可以这样做:

std::string s("hello, world!");

std::any a;
a = std::move(s);   // 把s移进a

s = std::move(std::any_cast<std::string&>(a));  // 把a中的string移动到s

注意像通常一样,值被移走的对象处于仍然有效但是值未定义的状态。因此,你可以继续将a用作字符串,只要不对它的值做任何假设。下面的语句将 不会 输出"NIL",值被移走的字符串一般是空字符串(但也可能有其他的值):

std::cout << (a.has_value() ? std::any_cast<std::string>(a) : std::string("NIL"));

注意:

s = std::any_cast<std::string>(std::move(a));

也能生效,但需要一次额外的move。直接转换成右值引用将不能通过编译:

s = std::any_cast<std::string&&>(a);    // 编译期error

注意和如下调用

a = std::move(s);   // 把s移进a

相比,下面的代码 有可能不能 工作(即使这是C++标准里的一个例子):

std::any_cast<string&>(a) = std::move(s); // OOPS:a必须持有string

只有当a已经包含有一个std::string类型的值时这段代码才能工作。否则,在我们赋予新值之前,这个转换就会抛出std::bad_any_cast异常。

参考

[1] std::any: How, when, and why

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 在 Objective-C 中,使用 `std::string` 会报错的原因是 Objective-C 和 C++ 是两种不同的编程语言,它们的语法和特性不完全兼容。 Objective-C 是一种面向对象的编程语言,它是在 C 语言的基础上发展而来的,并且主要用于开发 macOS 和 iOS 平台的应用程序。Objective-C 的字符串类型是 `NSString`,它是 Foundation 框架提供的一个类,具有很多方便的字符串操作方法。 而 `std::string` 是 C++ 标准库提供的一个类,用于处理字符串。C++ 是一种支持面向对象编程的通用编程语言,与 C 语言相比,具有更多的高级特性和功能。 在 Objective-C 中,如果想要使用 `std::string`,需要进行一些额外的工作。首先,需要在文件的开头添加 `#import <string>` 语句来引入 C++ 的标准库。然后,需要使用 `[[NSString stringWithCString:stdString.c_str() encoding:NSUTF8StringEncoding]]` 这样的方法将 `std::string` 转换成 `NSString` 对象。 如果没有正确导入 C++ 的标准库或者没有进行正确的类型转换,使用 `std::string` 就会报错。所以,在 Objective-C 中使用 `std::string` 之前,需要确保已经正确导入标准库并进行了适当的类型转换。 总之,Objective-C 和 C++ 是两种不同的编程语言,对应于不同的字符串类型。在 Objective-C 中使用 `std::string` 会报错,需要进行额外的导入和类型转换才能正确使用。 ### 回答2: 在Objective-C中,无法直接使用std::string,因为std::string是C++中的标准库类型,并不是Objective-C的一部分。 如果在Objective-C代码中使用std::string,会导致编译错误,因为Objective-C编译器无法找到std::string的定义。 解决这个问题的一种方法是将std::string转换为NSString。可以使用NSString的initWithUTF8String方法将std::string转换为NSString对象。具体代码如下: ``` std::string cppString = "Hello, World!"; NSString* objcString = [[NSString alloc] initWithUTF8String:cppString.c_str()]; ``` 这样就可以使用objcString来表示相应的字符串了。 另外,如果需要在Objective-C中频繁使用C++的字符串操作,可以考虑将整个文件的后缀名改为.mm,这样文件就会被当做Objective-C++文件处理,这样就可以使用std::string了。 总之,Objective-C中无法直接使用std::string,但可以通过将其转换为NSString来使用。 ### 回答3: 在 Objective-C 中使用 std::string 可能会导致编译错误。这是因为 std::string 是 C++ 标准库中的类,而 Objective-C 是基于 C 的编程语言。虽然 Objective-C 允许使用 C++ 的一些特性,但不支持完全的 C++语法。 如果你在 Objective-C 中使用 std::string,并且遇到了编译错误,可能是因为编译器无法识别 std::string 这个类型。你可以尝试进行以下解决办法: 1. 使用 NSString 类型代替 std::string 类型。NSString 是 Objective-C 中用于处理字符串的类,可以满足大部分的字符串操作需求。 2. 将 std::string 转换为 NSString。可以使用 NSString 的 initWithCString:encoding: 方法将 std::string 转换为 NSString 对象,然后在 Objective-C 中进行操作。 3. 在 Objective-C++ 文件中使用 std::string。如果你的文件后缀为 .mm,那么这个文件实际上是 Objective-C++ 文件,支持 C++ 语法。在这种文件中,可以直接使用 std::string,而不会报错。 需要注意的是,在使用 std::string 时,还要考虑到 Objective-C 的内存管理机制。如果使用了 std::string,可能需要手动管理内存的释放和分配,以避免内存泄漏等问题。 总而言之,当在 Objective-C 中出现 std::string 报错时,可以尝试使用 NSString 类型代替,转换为 NSString 对象,或者将文件后缀改为 .mm 来解决问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-西门吹雪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值