C++17 Feature Examples
1. Structured Binding
Structured bindings allow you to instantiate multiple entities at the same time with elements or members of an object.
Structured bindings allow you to declare a variable while simultaneously deconstructing a data structure of a composite type (e.g., a struct, astd::tuple
, std::pair
Orstd::array
). This makes it easy to get multiple values without explicitly calling thestd::tie()
or.get()
Methods.
Using structured bindings can greatly improve the readability of your code:
#include <iostream>;
#include <string>
#include <unordered_map>.
int main() {
std::unordered_map<std::string,std::string> mymap;
("k1", "v1");
("k2", "v2").
("k2", "v3").
for (const auto& elem : mymap)
std::cout << "old: " << << " : " << << std::endl;
for (const auto& [key,value] : mymap)
std::cout << " new: " << "key: " << key << ", value: " << value << std::endl;
return 0;
}
### A closer look at structured bindings
In order to understand structured binding, it is important to realize that there is actually a hidden anonymous object. The new variable names introduced during structured binding actually point to members/elements of this anonymous object.
Binding to an anonymous entity
The exact behavior of the following initialization:
struct MyStruct {
int i = 0;
std::string s;
}; struct MyStruct { int i = 0; std::string s; }
MyStruct ms.
auto [u, v] = ms; }; MyStruct; auto [u, v] = ms.
Equivalently, we initialize a new entity e with ms and make u and v in the structured binding aliases for members of e, similar to the following definition:
auto e = ms;
aliasname u = ;
aliasname v = ;
This means that u and v are simply aliases of members of a local copy of ms. However, we have not declared a name for e, so we cannot access this anonymous object directly. Note that u and v are not references to and (but rather their aliases). decltype(u) results in the type of member i, and decllytpe(v) results in the type of member s. Thus:
std::cout << u << ' ' << v << '\n'.
will print out and (copies of and , respectively).
The lifecycle of e is the same as that of structured bindings, and e is automatically destroyed when a structured binding leaves scope. Also, modifying variables used for initialization does not affect variables introduced by structured bindings (and vice versa), unless references are used
### Example code
```cpp
#include <iostream>
#include <tuple>
#include <vector>.
#include <string>.
#include <map>.
#include <unordered_map>.
struct MyStruct {
struct MyStruct { int num {1}; std::string str {"string")
std::string str {"test"};
};
int main() {
// Extract the value from the tuple using structured bindings
std::tuple<int, double, std::string> data = std::make_tuple(1, 3.14, "hello");
auto [a, b, c] = data;
std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;
//value
MyStruct my_struc;
auto [num,str] = my_struc;
std::cout << "num: " << num << ", str: " << str << std::endl;
my_struc.num = 2;
str = "test2";
std::cout << "num: " << num << ", str: " << str << std::endl;
//ref
MyStruct my_struc2 {2, "test2"};
const auto& [num2,str2] = my_struc2;
std::cout << "num2: " << num2 << ", str2: " << str2 << std::endl;
my_struc2.num = 4;
std::cout << "num2: " << num2 << ", str2: " << str2 << std::endl;
return 0;
}
### Summary
Theoretically, structured bindings work with any structure with public data members, C-style arrays, and "tuple-like" pairs.
objects":
- For structures and classes where all non-static data members are public, you can bind each member to a new variable name.
- For native arrays, you can bind each element of the array to a new variable name.
- For any type, you can use the tuple-like API to bind to a new name, regardless of how the API defines "element". For a
type, the API requires the following components:
- std::tuple_size<type>::valueTo return the number of elements.
- std::tuple_element<idx, type>::type to return the type of the idxth element.
- A global or member function get<idx>() to return the value of the idxth element.
The standard library types std::pair<>, std::tuple<>, std::array<> are examples that provide these APIs. If structures and classes provide tuple-like APIs, they will be bound using those APIs instead of binding data members directly.
## 2. Copy Elision
Technically, C++17 introduces a new rule: when passing or returning a temporary object by value, a copy of the temporary object must be omitted.
Effectively, we are actually passing an unmaterialized object.
Since the first standard, C++ has allowed copying to be elided in some cases, even if doing so might affect the outcome of the program (for example, a print statement in the copy constructor might not be executed). This can easily happen when initializing a new object with a temporary object, especially if a function passes or returns the temporary object by value. For example:
```cpp
#include <iostream>.
#include <tuple>
#include <vector>.
#include <string>.
#include <map>.
#include <complex>.
class MyClass
{
public.
// no copy/move constructor definition
MyClass(const MyClass&) = delete;
MyClass(MyClass&&) = delete;
};
MyClass bar() {
return MyClass{}; // return temporary object
}
int main() {
MyClass x = bar(); // initialize x with the returned temporary object
return 0; // initialize x with the returned temporary object.
}
The above code will give an error with c++14, c++17 doesn't give an error any more
However, note that other optional copy omission scenarios are still optional, and a copy or move constructor is still required in these scenarios. For example:
MyClass foo()
{
MyClass obj; ...
...
return obj; // still need support for copy/move constructor functions
}
Here, foo() has a named variable obj (which is lvalue when used). Therefore, the named returnvalue optimization (NRVO) takes effect, but the optimization still requires copy/move support. This also happens when obj is a formal parameter:
MyClass bar(MyClass obj) // Omit copying when passing a temporary variable.
copy(MyClass bar(MyClass obj))
...
return obj; // Copy/move support is still needed.
}
Copy/move support is no longer needed when passing a temporary variable (a.k.a. a pure right-value (prvalue)) as a real parameter, but it is still needed if this parameter is returned, because the returned object is named.
### What it does
One of the obvious effects of this feature is that less copying leads to better performance. Although many major compilers have optimized for this before, this behavior is now guaranteed by the standard. Although the move semantics significantly reduces the copying overhead, there are still significant performance gains to be had if no copies are made at all (e.g., when an object has many members of a basic type, the move semantics still has to copy each member). Additionally this feature can reduce the use of output parameters and instead return a value directly (provided that the value is created directly in the return statement). Another effect is that it is possible to define a factory function that always works, because now it can even return objects that do not allow copying or moving.
For example, consider the following generalized factory function:
``cpp
#include <iostream>.
#include <tuple>
#include <vector>.
#include <string>.
#include <map>.
#include <complex>.
#include <utility> #include <memory> #include <memory>
#include <memory>.
#include <memory> #include <atomic>.
template <typename T, typename... Args>
T create(Args&&... Args)
{
return T{std::forward<Args>(args)...} ;
}
int main() {
int i = create<int>(42);
std::unique_ptr<int> up = create<std::unique_ptr<int>>(new int{42});
std::atomic<int> ai = create<std::atomic<int>>(42);
std::cout << "ai: " << ai << std::endl;
return 0;
}