β

std::any: How, when, and why

Visual C++ Team Blog 51 阅读
C++

This post is part of a regular series of posts where the C++ product team here at Microsoft and other guests answer questions we have received from customers. The questions can be about anything C++ related: MSVC toolset, the standard language and library, the C++ standards committee, isocpp.org, CppCon , etc. Today’s post is by Casey Carter.

C++17 adds several new “vocabulary types” – types intended to be used in the interfaces between components from different sources – to the standard library. MSVC has been shipping implementations of std::optional , std::any , and std::variant since the Visual Studio 2017 release, but we haven’t provided any guidelines on how and when these vocabulary types should be used. This article on std::any is the second of a series that examines each of the vocabulary types in turn.

Storing arbitrary user data

Say you’re creating a calendar component that you intend to distribute in a library for use by other programmers. You want your calendar to be usable for solving a wide array of problems, so you decide you need a mechanism to associate arbitrary client data with days/weeks/months/years. How do you best implement this extensibility design requirement?

A C programmer might add a void* to each appropriate data structure:

struct day { 
  // ...things... 
  void* user_data; 
}; 

struct month { 
  std::vector<day> days; 
  void* user_data; 
};

and suggest that clients hang whatever data they like from it. This solution has a few immediately apparent shortcomings:

The C++ Standard Library provides us with at least one tool that can help: shared_ptr<void> . Replacing the void* with shared_ptr<void> solves the problem of lifetime management:

struct day {
  // ...things...
  std::shared_ptr<void> user_data;
};

struct month {
  std::vector<day> days;
  std::shared_ptr<void> user_data;
};

since shared_ptr squirrels away enough type info to know how to properly destroy the object it points at. A client could create a shared_ptr<Foo> , and the deleter would continue to work just fine after converting to shared_ptr<void> for storage in the calendar:

some_day.user_data = std::make_shared<std::string>("Hello, world!");
// ...much later...
some_day = some_other_day; // the object at which some_day.user_data _was_
                           // pointing is freed automatically

This solution may help solve the copyability problem as well, if the client is happy to have multiple days/weeks/etc. hold copies of the same shared_ptr<void> – denoting a single object – rather than independent values. shared_ptr doesn’t help with the primary problem of type-safety, however. Just as with void* , shared_ptr<void> provides no help tracking the proper type for associated data. Using a shared_ptr instead of a void* also makes it impossible for clients to “hack the system” to avoid memory allocation by reinterpreting integral values as void* and storing them directly; using shared_ptr forces us to allocate memory even for tiny objects like int .

Not just any solution will do

std::any is the smarter void* / shared_ptr<void> . You can initialize an any with a value of any copyable type:

std::any a0; 
std::any a1 = 42; 
std::any a2 = month{"October"};

Like shared_ptr , any remembers how to destroy the contained value for you when the any object is destroyed. Unlike shared_ptr , any also remembers how to copy the contained value and does so when the any object is copied:

std::any a3 = a0; // Copies the empty any from the previous snippet
std::any a4 = a1; // Copies the "int"-contain