CPP17 in Detail Notes - yszheda/wiki GitHub Wiki
For the direct initialisation, C++17 introduces new rules:
- For a braced-init-list with only a single element, auto deduction will deduce from that entry;
- For a braced-init-list with more than one element, auto deduction will be ill-formed.
auto x1 = { 1, 2 }; // decltype(x1) is std::initializer_list<int>
auto x2 = { 1, 2.0 }; // error: cannot deduce element type
auto x3{ 1, 2 }; // error: not a single element
auto x4 = { 3 }; // decltype(x4) is std::initializer_list<int>
auto x5{ 3 }; // decltype(x5) is int
Now, with C++17, function chaining will work as expected when they contain inner expressions, i.e., they are evaluated from left to right: In an expression:
a(expA).b(expB).c(expC)
expA is evaluated before calling b.
the following expressions are evaluated in the order a, then b:
- a.b
- a->b
- a->*b
- a(b1, b2, b3) // b1, b2, b3 - in any order
- b @= a // '@' means any operator
- a[b]
- a << b
- a >> b
- ES.43: Avoid expressions with undefined order of evaluation
- ES.44: Don’t depend on order of evaluation of function arguments
- Return Value Optimisation - RVO (disable RVO for
gcc
:-fno-elide-constructors
) - Named Return Value Optimisation - NRVO
Currently, the standard allows eliding in cases like:
- when a temporary object is used to initialise another object (including the object returned by a function, or the exception object created by a throw-expression)
- when a variable that is about to go out of scope is returned or thrown
- when an exception is caught by value
Why might this be useful?
- to allow returning objects that are not movable/copyable - because we could now skip copy/move constructors
- to improve code portability - as every conformant compiler supports the same rule
- to support the ‘return by value’ pattern rather than using output arguments
- to improve performance
In C++17 copy elision works only for temporary objects, not for Named RVO.
- lvalue - an expression that has an identity, and which we can take the address of
- xvalue - “eXpiring lvalue” - an object that we can move from, which we can reuse. Usually, its lifetime ends soon
- prvalue - pure rvalue - something without a name, which we cannot take the address of, we can move from such expression
- glvalue - “generalised” lvalue - A glvalue is an expression whose evaluation computes the location of an object, bit-field, or function
- prvalue - “pure” rvalue - A prvalue is an expression whose evaluation initialises an object, bit-field, or operand of an operator, as specified by the context in which it appears
In short: prvalues perform initialisation, glvalues describe locations.
The C++17 Standard specifies that when there’s a prvalue of some class or array there’s no need to create any temporary copies, the initialisation from that prvalue can happen directly. There’s no move or copy involved (so there’s no need to have a copy and move constructors); the compiler can safely do the elision.
It can happen:
- in initialisation of an object from a prvalue:
Type t = T()
- in a function call where the function returns a prvalue.
There are several exceptions where the temporary is still needed:
- when a prvalue is bound to a reference
- when member access is performed on a class prvalue
- when array subscripting is performed on an array prvalue
- when an array prvalue is decayed to a pointer
- when a derived-to-base conversions performed on a class prvalue
- when a prvalue is used as a discarded value expression
void (*p)();
void (**pp)() noexcept = &p; // error: cannot convert to
// pointer to noexcept function
struct S { typedef void (*p)(); operator p(); };
void (*q)() noexcept = S(); // error: cannot convert to
// pointer to noexcept
// before C++17
std::set<int> mySet;
std::set<int>::iterator iter;
bool inserted;
std::tie(iter, inserted) = mySet.insert(10);
if (inserted)
{
std::cout << "Value was inserted\n";
}
// since C++17
std::set<int> mySet;
auto [iter, inserted] = mySet.insert(10);
Structured Binding Declaration cannot be declared constexpr
(might be solved in C++20 by the proposal P1481)
- If the initializer is an array
- If the initializer supports
std::tuple_size<>
and providesget<N>()
andstd::tuple_-element
functions
class UserEntry {
public:
void Load() { }
std::string GetName() const { return name; }
unsigned GetAge() const { return age; }
private:
std::string name;
unsigned age { 0 };
size_t cacheEntry { 0 }; // not exposed
};
// The interface for Structured Bindings
// with if constexpr:
template <size_t I> auto get(const UserEntry& u) {
if constexpr (I == 0) return u.GetName();
else if constexpr (I == 1) return u.GetAge();
}
namespace std {
template <> struct tuple_size<UserEntry> : std::integral_constant<size_t, 2> { };
template <> struct tuple_element<0,UserEntry> { using type = std::string; };
template <> struct tuple_element<1,UserEntry> { using type = unsigned; };
}
// Usage example:
UserEntry u;
u.Load();
auto [name, age] = u; // read access
std:: cout << name << ", " << age << '\n';
- If the initializer’s type contains only non static, public members. The class doesn’t have to be POD, but the number of identifiers must equal the number of non-static data members.
struct Point {
double x;
double y;
};
Point GetStartPoint() {
return { 0.0, 0.0 };
}
const auto [x, y] = GetStartPoint();
-
if (init; condition)
-
switch (init; condition)
// inside a header file:
struct MyClass
{
static const int sValue;
};
// later in the same header file:
inline int const MyClass::sValue = 777;
// there’s no need to use constexpr
struct MyClass
{
inline static const int sValue = 777;
};
Note, that partial deduction cannot happen, you have to specify all the template parameters or none:
std::tuple t(1, 2, 3); // OK: deduction
std::tuple<int,int,int> t(1, 2, 3); // OK: all arguments are provided
std::tuple<int> t(1, 2, 3); // Error: partial deduction
// control block and int might be in different places in memory
std::shared_ptr<int> p(new int{10});
// the control block and int are in the same contiguous memory section
auto p2 = std::make_shared<int>(10);
template<typename T>
struct MyType
{
T str;
};
// custom deduction guide
MyType(const char *) -> MyType<std::string>;
MyType t{"Hello World"};
template<class... Ts>
struct overload : Ts... { using Ts::operator()...; };
template<class... Ts>
overload(Ts...) -> overload<Ts...>; // deduction guide
template<typename ...Args>
void FoldSeparateLine(Args&&... args)
{
auto separateLine = [](const auto& v) {
std::cout << v << '\n';
};
(... , separateLine (std::forward<Args>(args))); // over comma operator
}
// In C++11/C++14
template <typename Type, Type value> constexpr Type TConstant = value;
constexpr auto const MySuperConst = TConstant<int, 100>;
// Since C++17
template <auto value> constexpr auto TConstant = value;
constexpr auto const MySuperConst = TConstant<100>;
template <auto ... vs> struct HeterogenousValueList {};
using MyList = HeterogenousValueList<'a', 100, 'b'>;
template<class... Ts> struct overloaded : Ts... {
using Ts::operator()...;
};
-
template<class... B> struct conjunction;
- logical AND -
template<class... B> struct disjunction;
- logical OR -
template<class B> struct negation;
- logical negation
C++11:
[[noreturn]]
-
[[carries_dependency]]
: Indicates that the dependency chain in release-consume std::memory_order propagates in and out of the function, which allows the compiler to skip unnecessary memory fence instructions. Mostly to help to optimise multi-threaded code and when using different memory models.
C++14:
-
[[deprecated]]
and[[deprecated("reason")]]
C++17:
[[fallthrough]]
[[maybe_unused]]
[[nodiscard]]
enum class [[nodiscard]] ErrorCode {
OK,
Fatal,
System,
FileIssue
};
ErrorCode OpenFile(std::string_view fileName);
ErrorCode SendEmail(std::string_view sendto, std::string_view text);
ErrorCode SystemCall(std::string_view text);
There is a reasonable fear that attributes will be used to create language dialects. The recommendation is to use attributes to only control things that do not affect the meaning of a program but might help detect errors (e.g.
[[noreturn]]
) or help optimizers (e.g.[[carries_dependency]]
).
- Initialise as empty
- Directly with a value
- With a value using deduction guides
- By using
make_optional
- With
std::in_place
- From other
optional
So what’s the advantage of using std::in_place_t
in std::optional
?
There are at least two important reasons:
- Default constructor
- Efficient construction for constructors with many arguments
auto opt = std::make_optional<UserName>();
auto opt = std::make_optional<Point>(0, 0);
// Is as efficient as:
std::optional<UserName> opt{std::in_place};
std::optional<Point> opt{std::in_place_t, 0, 0};
std::optional<std::string> CreateString()
{
std::string str {"Hello Super Awesome Long String"};
return {str}; // this one will cause a copy
// return str; // this one moves
}
According to the Standard if you wrap a return value into braces {} then you prevent move operations from happening. The returned object will be copied only.
-
operator*
andoperator->
- if there’s no value the behaviour is undefined! -
value()
- returns the value, or throwsstd::bad_optional_access
-
value_or(defaultVal)
- returns the value if available, or defaultVal otherwise
// compute string function:
std::optional<std::string> maybe_create_hello();
// ...
if (auto ostr = maybe_create_hello(); ostr)
{
std::cout << "ostr " << *ostr << '\n';
}
else
{
std::cout << "ostr is null\n";
}
template <typename T>
class optional
{
bool _initialized;
std::aligned_storage_t<sizeof(t), alignof(T)> _storage;
public:
// operations
};
-
std::optional
is a wrapper type to express “null-able” types -
std::optional
won’t use any dynamic allocation -
std::optional
contains a value or it’s empty – useoperator *
,operator-
>,value()
orvalue_or()
to access the underlying value. -
std::optional
is implicitly converted to bool so that you can easily check if it contains a value or not
C.183: Don’t use a
union
for type punningReason: It is undefined behaviour to read a union member with a different type from the one with which it was written. Such punning is invisible, or at least harder to spot than using a named cast. Type punning using a union is a source of errors.
- All the places where you might get a few types for a single field: so things like parsing command lines, ini files, language parsers, etc.
- Expressing several possible outcomes of a computation efficiently: like finding roots of equations.
- Error handling - for example you can return
variant<Object, ErrorCode>
. If the value is available, then you return Object otherwise you assign some error code. - Finite State Machines.
- Polymorphism without vtables and inheritance (thanks to the visitor pattern).
variant types (also called a tagged union, a discriminated union, or a sum type) come from the functional language world and Type Theory
when the first alternative doesn’t have a default constructor, you can place std::monostate
as the first alternative (or you can also shuffle the types, and find the type with a default constructor).
// monostate for default initialisation:
class NotSimple
{
public:
NotSimple(int, float) { }
};
// std::variant<NotSimple, int> cannotInit; // error
std::variant<std::monostate, NotSimple, int> okInit;
std::cout << okInit.index() << '\n';
- the assignment operator
emplace
-
get
and then assign a new value for the currently active type - a visitor (you cannot change the type, but you can change the value of the current alternative)
std::variant<std::string, int> v { "Hello A Quite Long String" };
// v allocates some memory for the string
v = 10; // we call destructor for the string!
// no memory leak
template <class Visitor, class... Variants>
constexpr visit(Visitor&& vis, Variants&&... vars);
auto PrintVisitor = [](const auto& t) { std::cout << t << '\n'; };
auto TwiceMoreVisitor = [](auto& t) { t*= 2; };
std::variant<int, float> intFloat { 20.4f };
std::visit(PrintVisitor, intFloat);
std::visit(TwiceMoreVisitor, intFloat);
std::visit(PrintVisitor, intFloat);
struct MultiplyVisitor
{
float mFactor;
MultiplyVisitor(float factor) : mFactor(factor) { }
void operator()(int& i) const {
i *= static_cast<int>(mFactor);
}
void operator()(float& f) const {
f *= mFactor;
}
void operator()(std::string& ) const {
// nothing to do here...
}
};
std::visit(MultiplyVisitor(0.5f), intFloat);
std::visit(PrintVisitor, intFloat);
template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>;
std::variant<int, float, std::string> myVariant;
std::visit(
overload {
[](const int& i) { std::cout << "int: " << i; },
[](const std::string& s) { std::cout << "string: " << s; },
[](const float& f) { std::cout << "float: " << f; }
},
myVariant
);
overload
uses three C++17 features:
-
Pack expansions in using declarations - short and compact syntax with variadic templates.
-
Custom template argument deduction rules - this allows the compiler to deduce types of lambdas that are the base classes for the pattern. Without it we’d have to define a “make” function.
-
Extension to aggregate Initialisation - the overload pattern uses aggregate initialisation to init base classes. Before C++17 it was not possible.
std::variant
uses the memory in a similar way to union: so it will take the max size of the underlying types. But since we need something that will know what the currently active alternative is, then we need to use some more space.
What’s more interesting is that std::variant
won’t allocate any extra space! No dynamic allocation happens to hold variants or the discriminator.
- It holds one of several alternatives in a type-safe way
- No extra memory allocation is needed. The
variant
needs the size of the max of the sizes of the alternatives, plus some little extra space for knowing the currently active value - By default, it initialises with the default value of the first alternative
- You can assess the value by using
std::get
, std::get_if`` or by using a form of a visitor - To check the currently active type you can use
std::holds_alternative
orstd::variant::index
-
std::visit
is a way to invoke an operation on the currently active type in thevariant
. It’s a callable object with overloads for all the possible types in thevariant
(s) - Rarely
std::variant
might get into invalid state, you can check it viavalueless_by_
-exception
-
std::any
is not a template class likestd::optional
orstd::variant
. - by default it contains no value, and you can check it via
.has_value()
. - you can reset an any object via
.reset()
. - it works on “decayed” types - so before assignment, initialisation, or emplacement the type is transformed by
std::decay
. - when a different type is assigned, then the active type is destroyed.
- you can access the value by using
std::any_cast<T>
. It will throwbad_any_cast
if the active type is notT
. - you can discover the active type by using
.type()
that returnsstd::type_info
of the type.
While void*
might be an extremely unsafe pattern with some limited use cases, std::any
adds type-safety, and that’s why it has more applications.
- a default initialisation - then the object is empty
- a direct initialisation with a value/object
- in place
std::in_place_type
- via
std::make_any
The crucial part of being safe for std::any
is not to leak any resources. To achieve this behaviour std::any
will destroy any active object before assigning a new value.
template<class ValueType> ValueType std::any_cast
:
- read access - returns a copy of the value, and throws
std::bad_any_cast
when it fails - read/write access - returns a reference, and throws
std::bad_any_cast
when it fails - read/write access - returns a pointer to the value (
const
or not) ornullptr
on failure
-
std::any
requires extra dynamic memory allocations. - Implementations are encouraged to use SBO - Small Buffer Optimisation.
-
std::any
is not a template class -
std::any
uses Small Buffer Optimisation, so it will not dynamically allocate memory for simple types likeint
s,double
s… but for larger types, it will use extra new. -
std::any
might be considered ‘heavy’, but offers a lot of flexibility and type-safety. - you can access the currently stored value by using
any_cast
that offers a few “modes”: for example it might throw an exception or returnnullptr
. - use it when you don’t know the possible types - in other cases consider
std::variant
.
-
Optimisation: you can carefully review your code and replace various string operations with
string_view
. In most cases, you should end up with faster code and fewer memory allocations. -
As a possible replacement for
const std::string&
parameter - especially in functions that don’t need the ownership and don’t store the string. -
Handling
string
s coming from other API:QString
,CString
,const char*
... everything that is placed in a contiguous memory chunk and has a basic char-type. You can write a function that acceptsstring_view
and no conversion from that other implementation will happen. -
string_view
is only a non-owning view, so if the original object is gone, the view becomes rubbish and you might get into trouble. -
string_view
might not contain null terminator
-
string_view
is problematic with all functions that accept traditional C-strings becausestring_view
breaks with C-string termination assumptions. If a function accepts only aconst char*
parameter, it’s probably a bad idea to passstring_view
into it. On the other hand, it might be safe when such a function acceptsconst char*
and length parameters. - Conversion into
string
s - you need to specify not only the pointer to the contiguous character sequence but also the length.
the lifetime of a temporary object bound to a const reference is prolonged to the lifetime of the reference itself.
std::string func()
{
std::string s;
// build s...
return s;
}
std::string_view sv = func();
// no temp lifetime extension!
std::string s = "Hello World";
std::string_view sv = s;
std::string_view sv2 = sv.substr(0, 5);
printf("My String %s", sv2.data()); // oops?
printf("%.*s\n", static_cast<int>(sv2.size()), sv2.data()); // fix
std::string number = "123.456";
std::string_view svNum { number.data(), 3 };
auto f = atof(svNum.data()); // should be 123, but is 123.456!
std::cout << f << '\n';
// Fix: use from_chars (C++17)
int res = 0;
std::from_chars(svNum.data(), svNum.data()+svNum.size(), res);
std::cout << res << '\n';
// Fix (General solution)
std::string tempStr { svNum.data(), svNum.size() };
auto f = atof(tempStr.c_str());
std::cout << f << '\n';
-
string_view
is usually implemented as [ptr, len] - one pointer and usuallysize_t
to represent the possible size.
If we consider the
std::string
type, due to common Small String Optimisationsstd::string
is usually 24 or 32 bytes, so double the size ofstring_view
. If astring
is longer than the SSO buffer thenstd::string
allocates memory on the heap. If SSO is not supported (which is rare), thenstd::string
would consist of a pointer to the allocated memory and the size.
-
substr
is just a copy of two elements instring_view
, whilestring
will perform a copy of a memory range. The complexity is O(1) vs O(n).
std::vector<std::string_view>
splitSV(std::string_view strv, std::string_view delims = " ")
{
std::vector<std::string_view> output;
auto first = strv.begin();
while (first != strv.end())
{
const auto second = std::find_first_of(first, std::cend(strv), std::cbegin(delims), std::cend(delims));
if (first != second)
{
output.emplace_back(strv.substr(std::distance(strv.begin(), first), std::distance(first, second)));
}
if (second == strv.end()) break;
first = std::next(second);
}
return output;
}
// Usage example
const std::string str {
"Hello Extra,,, Super, Amazing World"
};
for (const auto& word : splitSV(str, " ,"))
{
std::cout << word << '\n';
}
- It’s a specialisation of
std::basic_string_view<charType, traits<charType>>
- with charType equal tochar
. - It’s a non-owning view of a contiguous sequence of characters.
- It might not include null terminator at the end.
- It can be used to optimise code and limit the need for temporary copies of strings.
- It contains most of
std::string
operations that don’t change the underlying characters. - Its operations are also marked as constexpr. But:
- Make sure the underlying sequence of characters is still present!
- While
std::string_view
looks like a constant reference to the string, the language doesn’t extend the lifetime of returned temporary objects that are bound tostd::string_view
. - Always remember to use
stringView.size()
when you build a string fromstring_view
. Thesize()
method properly marks the end ofstring_view
. - Be careful when you pass
string_view
into functions that accept null-terminated strings unless you’re sure yourstring_view
contains a null terminator.
-
Herb Sutter: The Free Lunch Is Over - A Fundamental Turn Toward Concurrency in Software
-
The Amazing Performance of C++17 Parallel Algorithms, is it Possible?
-
How to Boost Performance with Intel Parallel STL and C++17 Parallel Algorithms
- When using
par
execution policy try to access the shared resources as little as possible. - Don’t use synchronisation and memory allocation when executing with
par_unseq
policy.
- Parallel STL gives you set of 69 algorithms that have overloads for the execution policy parameter.
- Execution policy describes how the algorithm might be executed.
- There are three execution policies in C++17 (
<execution>
header) –std::execution::seq
- sequential –std::execution::par
- parallel –std::execution::par_unseq
- parallel and vectorised - In parallel execution policy functors that are passed to algorithms cannot cause deadlocks and data races
- In parallel unsequenced policy functors cannot call vectorised unsafe instructions like memory allocations or any synchronisation mechanisms
- To handle new execution patterns there are also new algorithms: like
std::reduce
,excelusive_scan
- They work out of order so the operations must be associative to generate deterministic results - There are “fused” algorithms:
transform_reduce
,transform_exclusive_scan
,transform_inclusive_scan
that combine two algorithms together. - Assuming there are no synchronisation points in the parallel execution, the parallel algorithms should be faster than the sequential version. Still, they perform more work - especially the setup and divisions into tasks.
- Implementations might usually use some thread pools to implement a parallel algorithm on CPU.
-
try_emplace()
- if the object already exists then it does nothing, otherwise it behaves likeemplace()
. –emplace()
might move from the input parameter when the key is in the map, that’s why it’s best to usefind()
before such emplacement. -
insert_or_assign()
- gives more information thanoperator[]
- as it returns if the element was newly inserted or updated and also works with types that have no default constructor.
// since C++11 and until C++17 for std::vector
template< class... Args >
void emplace_back( Args&&... args );
// since C++17 for std::vector
template< class... Args >
reference emplace_back( Args&&... args );
-
std::gcd
(Greatest Common Divisor) -
std::lcm
(Least Common Multiple) std::clamp
-
std::pmr::memory_resource
- is an abstract base class for all other implementations. It defines the following pure virtual methods:do_allocate
,do_deallocate
anddo_is_equal
. -
std::pmr::polymorphic
allocator - is an implementation of a standard allocator that usesmemory_resource
object to perform memory allocations and deallocations. -
global memory resources accessed by
new_delete_resource()
andnull_memory_resource()
-
a set of predefined memory pool resource classes:
synchronized_pool_resource
,unsynchronized_pool_resource
, andmonotonic_buffer_resource
-
template specialisations of the standard containers with polymorphic allocator, for example
std::pmr::vector
,std::pmr::string
,std::pmr::map
and others. Each specialisation is defined in the same header file as the corresponding container.