您需要了解的所有有关性病的知识::任何

哈Ha!我们提请您注意Bartlomiej Filipek撰写的文章“关于std ::从C ++ 17开始的所有知识”译文

图片

std::optional的帮助下可以存储一种类型。std::variant的帮助下,可以将多个类型存储在一个对象中。C ++ 17为我们提供了另一种此类包装器类型- std::any可以存储任何内容,同时保持类型安全。

基础


在此之前,C ++标准并未为将多个类型存储在一个变量中的问题提供许多解决方案。当然可以使用void*,但是它绝对不安全。

从理论上讲,void*您可以将其包装在一个可以以某种方式存储类型的类中:

class MyAny
{
    void* _value;
    TypeInfo _typeInfo;
};

如您所见,我们有了某种基本形式std::any,但是要确保类型安全,MyAny我们需要进行其他检查。这就是为什么使用标准库中的选项胜于做出自己的决定的原因。

这就是std::anyC ++ 17的含义它允许您在对象中存储任何内容,并在尝试通过指定错误的类型进行访问时报告错误(引发异常)。

小样:

std::any a(12);

//    :
a = std::string("Hello!");
a = 16;
//   :

//    a  
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';
}

//        - :
a.reset();
if (!a.has_value())
{
    std::cout << "a is empty!" << "\n";
}

//    any  :
std::map<std::string, std::any> m;
m["integer"] = 10;
m["string"] = std::string("Hello World");
m["float"] = 1.0f;

for (auto &[key, val] : m)
{
    if (val.type() == typeid(int))
        std::cout << "int: " << std::any_cast<int>(val) << "\n";
    else if (val.type() == typeid(std::string))
        std::cout << "string: " << std::any_cast<std::string>(val) << "\n";
    else if (val.type() == typeid(float))
        std::cout << "float: " << std::any_cast<float>(val) << "\n";
}

此代码将输出:

16
bad any_cast
a is empty!
float: 1
int: 10
string: Hello World

上面的示例显示了一些重要的事情:

  • std::anystd::optional std::variant
  • , .has_value()
  • .reset()
  • std::decay
  • ,
  • std::any_cast, bad_any_cast, «T»
  • .type(), std::type_info

上面的示例令人印象深刻-C ++中的实型变量!如果您非常喜欢JavaScript,甚至可以制作所有类型变量std::any并将C ++用作JavaScript :)

但是,也许有一些正常的用法示例?

什么时候使用?


虽然void*我认为它是非常不安全的事情,可能的用途非常有限,但它是std::any完全类型安全的,因此它有一些很好的使用方法。

例如:

  • 在库中-当您的库需要存储或传输某些数据时,并且您不知道该数据可以是哪种类型
  • 解析文件时-如果您确实无法确定支持的类​​型
  • 讯息传递
  • 脚本语言交互
  • 为脚本语言创建解释器
  • 用户界面-字段可以存储任何内容

在我看来,在许多示例中,我们可以突出显示受支持类型的有限列表,因此它std::variant可能是一个更好的选择。但是,当然,如果不知道将要使用的最终产品,就很难创建库。您只是不知道将在其中存储什么类型。

该演示显示了一些基本内容,但是在以下各节中,您将了解更多有关的内容std::any,因此请继续阅读。

创建std ::任何


有几种方法可以创建类型的对象std::any

  • 标准初始化-对象为空
  • 使用值/对象直接初始化
  • 直接指示对象的类型- std::in_place_type
  • 通过 std::make_any

例如:

//  :
std::any a;
assert(!a.has_value());

//   :
std::any a2(10); // int
std::any a3(MyType(10, 11));

// in_place:
std::any a4(std::in_place_type<MyType>, 10, 11);
std::any a5{std::in_place_type<std::string>, "Hello World"};

// make_any
std::any a6 = std::make_any<std::string>("Hello World");

变更价值


std::any两种方法可以更改当前存储在方法emplace或赋值中的值:

std::any a;

a = MyType(10, 11);
a = std::string("Hello");

a.emplace<float>(100.5f);
a.emplace<std::vector<int>>({10, 11, 12, 13});
a.emplace<MyType>(10, 11);

对象生命周期


安全的关键std::any是资源的缺乏泄漏。为此,它将std::any在分配新值之前销毁所有活动对象。

std::any var = std::make_any<MyType>();
var = 100.0f;
std::cout << std::any_cast<float>(var) << "\n";

此代码将输出以下内容:

MyType::MyType
MyType::~MyType
100

该对象std::any使用MyType类型的对象初始化,但是在分配新值(100.0f)之前,将调用析构函数MyType

获得价值


在大多数情况下,只有一种方法可以访问std::any-中std::any_cast的值,如果它存储在对象中,它将返回指定类型的值。

此功能非常有用,因为它有多种使用方法:

  • 返回值的副本并std::bad_any_cast在出错时退出
  • 返回指向该值的链接,并std::bad_any_cast 在出错时退出
  • 在出现错误的情况下返回指向值的指针(是否为常数)或nullptr

看一个例子:

struct MyType
{
    int a, b;

    MyType(int x, int y) : a(x), b(y) { }

    void Print() { std::cout << a << ", " << b << "\n"; }
};

int main()
{
    std::any var = std::make_any<MyType>(10, 10);
    try
    {
        std::any_cast<MyType&>(var).Print();
        std::any_cast<MyType&>(var).a = 11; // /
        std::any_cast<MyType&>(var).Print();
        std::any_cast<int>(var); // throw!
    }
    catch(const std::bad_any_cast& e) 
    {
        std::cout << e.what() << '\n';
    }

    int* p = std::any_cast<int>(&var);
    std::cout << (p ? "contains int... \n" : "doesn't contain an int...\n");

    MyType* pt = std::any_cast<MyType>(&var);
    if (pt)
    {
        pt->a = 12;
        std::any_cast<MyType&>(var).Print();
    }
}

如您所见,我们有两种跟踪错误的方式:通过异常(std::bad_any_cast)或返回指针(或nullptr)。std::any_cast返回指针的函数已重载并标记为noexcept

性能和内存使用


std::any它看起来像一个功能强大的工具,您很可能会使用它来存储不同类型的数据,但是它的价格是多少?

主要问题是额外的内存分配。

std::variant 并且std::optional不需要任何其他的内存分配,但这是因为存储在对象中的数据类型是预先已知的。 std :: any没有此类信息,因此它可以使用其他内存。

这会永远发生还是有时发生?哪个规则?使用int之类的简单类型是否还会发生这种情况?

让我们看看标准的内容:
实现应避免为较小的包含值使用动态分配的内存。示例:构造的对象仅包含一个int。这种小对象优化只能应用于is_nothrow_move_constructible_v为true的类型T
一个实现应避免对小型存储的数据使用动态内存。例如,创建对象时仅存储int。这种对小对象的优化只能应用于is_nothrow_move_constructible_v为true的类型T。

结果,他们建议使用小型缓冲区优化/ SBO来实现。但这也有代价。这会使类型更大-覆盖缓冲区。

让我们看一下size std::any,这是几个编译器的结果:

编译器sizeof(任何)
GCC 8.1(Coliru)十六
lang 7.0.0(Wandbox)32
MSVC 2017 15.7.0 32位40
MSVC 2017 15.7.0 64位64

如您所见,通常std::any这不是一个简单的类型,并且会带来额外的费用。由于SBO,它通常会占用大量内存,从16到32个字节(在GCC或clang中,甚至在MSVC中是64个字节!)。

从boost迁移::任何


boost::any它是在2001年(版本1.23.0)引入的。此外,作者boost::any(Kevlin Henney)也是该提案的作者std::any因此,这两种类型密切相关,STL的版本强烈地基于其前身。

以下是主要更改:

功能Boost.Any
(1.67.0)
性病::任何
额外的内存分配
小对象优化没有
位置没有
in_place_type_t在构造函数中没有


主要区别在于它boost::any不使用SBO,因此占用的内存显着减少(在GCC8.1中,它的大小为8字节),但是正因为如此,它甚至可以为int这样的小类型动态分配内存。

使用std :: any的示例


主要优点std::any是灵活性。在下面的示例中,您可以看到一些想法(或特定实现),使用std::any它们使应用程序更容易些。

文件解析


在示例std::variant 您可以在此处看到它们)中,您可以看到如何解析配置文件并将结果存储在类型变量中std::variant现在您正在编写一个非常通用的解决方案,也许它是某些库的一部分,那么您可能不知道所有可能的类型类型。就性能而言,

使用std::anyfor参数存储数据可能会很好,同时又为您提供了解决方案的灵活性。

讯息传递


在主要用C语言编写的Windows Api中,有一个消息传递系统,它使用带有两个可选参数的消息ID来存储消息数据。基于此机制,您可以实现WndProc,它处理发送到窗口的消息。

LRESULT CALLBACK WindowProc(
  _In_ HWND   hwnd,
  _In_ UINT   uMsg,
  _In_ WPARAM wParam,
  _In_ LPARAM lParam
);

事实是,数据以wParamlParam以不同的形式存储有时您只需要使用几个字节wParam

如果我们更改此系统以使消息可以将任何内容传递给处理方法,该怎么办?

例如:

class Message
{
public:
    enum class Type 
    {
        Init,
        Closing,
        ShowWindow,        
        DrawWindow
    };

public:
    explicit Message(Type type, std::any param) :
        mType(type),
        mParam(param)
    {   }
    explicit Message(Type type) :
        mType(type)
    {   }

    Type mType;
    std::any mParam;
};

class Window
{
public:
    virtual void HandleMessage(const Message& msg) = 0;
};

例如,您可以向窗口发送消息:

Message m(Message::Type::ShowWindow, std::make_pair(10, 11));
yourWindow.HandleMessage(m);

窗口可以回复如下消息:

switch (msg.mType) {
// ...
case Message::Type::ShowWindow:
    {
    auto pos = std::any_cast<std::pair<int, int>>(msg.mParam);
    std::cout << "ShowWidow: "
              << pos.first << ", " 
              << pos.second << "\n";
    break;
    }
}

当然,您必须确定数据类型在消息中的存储方式,但是现在您可以使用实型而不是带有数字的不同技巧。

物产


任何代表C ++(N1939)的原始文档都显示了一个属性对象的示例:

struct property
{
    property();
    property(const std::string &, const std::any &);

    std::string name;
    std::any value;
};

typedef std::vector<property> properties;

该对象看起来非常有用,因为它可以存储许多不同的类型。我想到的第一个例子是在用户界面管理器或游戏编辑器中使用它的示例。

我们穿越边界


r / cpp中,有一个关于std :: any的流。至少有一个很棒的评论总结了何时应使用类型。

从这个评论
最重要的是,std :: any允许您跨不知道其类型的边界将权限转让给任意数据。
我之前谈论的一切都与这个想法很接近:

  • 在接口库中:您不知道客户端要使用哪种类型
  • 消息传递:相同的想法-给客户带来灵活性
  • 文件解析:支持任何类型


在本文中,我们学到了很多东西std::any

这里有一些事情要记住:

  • std::any 不是模板类
  • std::any 使用小对象的优化,因此它不会为int或double之类的简单类型动态分配内存,而对于较大的类型,将使用额外的内存
  • std::any 可以称为“沉重”,但它提供了安全性和更大的灵活性
  • std::any可以借助any_cast提供多种“模式”的访问数据例如,如果发生错误,则可能会引发异常或仅返回nullptr
  • 当您不确切知道可能的数据类型时使用它,否则请考虑使用 std::variant

All Articles