C++17 Guide

Cpp17TheCompleteGuide.pdf

BASIC LANGUAGE FEATURES

1. Structured Bindings

结构化绑定不支持嵌套声明或多维声明

结构化绑定声明中的变量数量都应该和元素或数据成员的数量保持一致

struct MyStruct {
    int i = 0;
    std::string s;
};

MyStruct ms;
auto [u,v] = ms;  // <--
for (const auto& [key,val] : mymap) {
    std::cout << key << ": " << val << '\n';
}
int arr[] = {47, 11};
auto [x, y] = arr;  // x and y are ints initialized by elems of arr
// 必须在数组长度已知的情况下使用结构化绑定

2. if and switch with Initialization

在下文代码中,变量 lg必须有变量名,否则会马上创建然后被销毁。

if (std::lock_guard<std::mutex> lg{collMutex}; !coll.empty()) {
    std::cout << coll.front() << '\n';
}

上下这两份代码等价。

{
    std::lock_guard<std::mutex> lg{collMutex};
    if (!coll.empty()) {
        std::cout << coll.front() << '\n';
    }
}
if (auto [pos,ok] = coll.insert({"new",40}); !ok) {
    // if insert failed, handle error using iterator pos:
    const auto& [key,val] = *pos;
    std::cout << "already there: " << key << '\n';
}

3. Inline Variables

you can define a variable/object in a header file as inline and if this definition is used by multiple translation units, they all refer to the same unique object

class MyClass {
    static inline std::string name = ""; // OK 
};

inline MyClass myGlobalObj; // OK even if included/defined by multiple CPP files

初始化变量之前要确保其类型是完整的

struct MyType {
    int value;
    MyType(int i) : value{i} {}
    // one static object to hold the maximum value of this type:
    static MyType max; // can only be declared here
};
inline MyType MyType::max{0};

4. Aggregate Extensions

struct Data {
    std::string name;
    double value;
};
struct MoreData : Data {
    bool done;
};

MoreData y{{"test1", 6.778}, false};

// 对于带成员的子对象的初始化,如果基本类型或子对象只获取一个值,可以跳过嵌套的大括号
MoreData y{"test1", 6.778, false};
MoreData u; // OOPS: value/done are uninitialized
MoreData z{}; // OK: value/done have values 0/false

To summarize, since C++17 an aggregate is defined as:

  • either an array
  • or a class type (class, struct, or union) with:

    • no user-declared or explicit constructor
    • no constructor inherited by a using declaration
    • no private or protected non-static data members
    • no virtual functions
    • no virtual, private, or protected base classes

To be able to use an aggregate it is also required that no private or protected base class members
or constructors are used during initialization.

// a new type trait is_aggregate<> to test, whether a type is an aggregate

template<typename T>
    struct D : std::string, std::complex<T> {
    std::string data;
};

D<float> s{{"hello"}, {4.5,6.7}, "world"}; // OK since C++17
std::cout << std::is_aggregate<decltype(s)>::value; // outputs: 1 (true)

5. Mandatory Copy Elision or Passing Unmaterialized Objects

One benefit of this feature is, of course, guaranteed better performance when returning a value that is expensive to copy. Another benefit is the ability now to define a factory function that always works, because it can now also return an object even when neither copying nor moving is allowed. As another effect, for classes with explicitly deleted move constructors, you can now return temporaries by value and initialize objects with them.

// factory.hpp
#include <utility>
template <typename T, typename... Args>
T create(Args&&... args)
{
    ...
    return T{std::forward<Args>(args)...};
}

// factory.cpp
#include "factory.hpp"
#include <memory>
#include <atomic>
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);
    return 0;
}

6. Lambda Extensions

C++17 improved their abilities to allow the use of lambdas in even more places:

  • in constant expressions (i.e., at compile time)
  • in places where you need a copy of the current object (e.g., when calling lambdas in threads)
auto squared = [](auto val) {
    //static int calls = 0; disables lambda for constexpr contexts
    
    // implicitly constexpr since C++17
    return val*val;
};
std::array<int,squared(5)> a; // OK since C++17 => std::array<int,25>
// 这样写会被转换成下面的模式
auto squared = [](auto val) {
    return val*val;
};

class CompilerSpecificName {
    public:
    ...
    template<typename T>
    constexpr auto operator() (T val) const {
        return val*val;
    }
};
class C {
private:
    std::string name;
public:
    void foo() {
        auto l1 = [*this] { std::cout << name << '\n'; };
/*
    the capture *this means that a ->copy<- of the current object is passed to the lambda
*/
    }
};

7. New Attributes and Attribute Features

[[nodiscard]] The new attribute [[nodiscard]] can be used to encourage warnings by the compiler if a return
value of a function is not used.

主要用在不使用返回值就会发生错误行为的函数。例如可能会导致内存泄漏,意外或者非知觉的行为。也可以用于取消不必要的开销,例如不使用返回值就无操作的行为。

The new attribute [[maybe_unused]] can be used to avoid warnings by the compiler for not using a name or entity.

The new attribute [[fallthrough]] can be used to avoid warnings by the compiler for not having
a break statement after a sequence of one or more case labels inside a switch statement.

void commentPlace(int place)
{
    switch (place) {
        case 1:
            std::cout << "very ";
            [[fallthrough]];
        case 2:
            std::cout << "well\n";
        break;
        default:
            std::cout << "OK\n";
        break;
    }
}
// Attributes are now allowed to mark namespaces.
namespace [[deprecated]] DraftAPI {
    ...
}

// Attributes are now allowed to mark enumerators
enum class City { Berlin = 0,NewYork = 1,Mumbai = 2, 
                 Bombay [[deprecated]] = Mumbai,
... };

NEW LIBRARY COMPONENTS

1. std::optional<>

std::optional 也可以作为一个可选的参数传递给函数。

#include <optional>
#include <string>
#include <iostream>

// convert string to int if possible:
std::optional<int> asInt(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (...) {
        return std::nullopt;
    }
}

int main(){
    for (auto s : {"42", " 077", "hello", "0x33"} ) {
    // try to convert s to int and print the result if possible:
    std::optional<int> oi = asInt(s);
    if (oi) {
        std::cout << "convert '" << s << "' to int: " << *oi << "\n";
    } else {
        std::cout << "can't convert '" << s << "' to int\n";
    }
}
OperationEffect
constructorsCreate an optional object (might call constructor for containedtype)
make_optional<>()Create an optional object (passing value(s) to initialize it)
destructorDestroys an optional object
=Assign a new value
emplace()Assign a new value to the contained type
reset()Destroys any value (makes the object empty)
has_value()Returns whether the object has a value
conversion to boolReturns whether the object has a value
*Value access (undefined behavior if no value)
->Access to member of the value (undefined behavior if no value)
value()Value access (exception if no value)
value_or()Value access (fallback argument if no value)
swap()Swaps values between two objects
==, !=, <, <=, >, >=Compare optional objects
hash<>Function object type to compute hash values

2. std::variant<> - a new union class

Variants simply have internal memory for the maximum size of the underlying types plus some fixed overhead to manage which alternative is used. No heap memory is allocate.

#include <variant>
#include <iostream>
int main(){
    std::variant<int, std::string> var{"hi"}; // initialized with string alternative
    std::cout << var.index() << '\n'; // prints 1
    var = 42;  // now holds int alternative
    std::cout << var.index() << '\n'; // prints 0
    
    ...
    try {
        int i = std::get<0>(var); // access by index
        std::string s = std::get<std::string>(var); // access by type (throws excep-tion in this case)
        ...
    } catch (const std::bad_variant_access& e) {
        // in case a wrong type/index is used
        std::cerr << "EXCEPTION: " << e.what() << '\n';
        ...
    }
}

3. std::any

std::any is a value type that is able to change its type

std::any a; // a is empty
std::any b = 4.3; // b has value 4.3 of type double
a = 42; // a has value 42 of type int
b = std::string{"hi"}; // b has value "hi" of type std::string

if (a.type() == typeid(std::string)) {
    std::string s = std::any_cast<std::string>(a);
    useString(s);
} else if (a.type() == typeid(int)) {
    useInt(std::any_cast<int>(a));
}

4. std::byte

#include <cstddef> // for std::byte
std::byte b1{0x3F};
std::byte b2{0b1111’0000};
std::byte b3[4] {b1, b2, std::byte{1}}; // 4 bytes (last is 0)

if (b1 == b3[0]) {
    b1 <<= 1;
}
std::cout << std::to_integer<int>(b1) << '\n'; // outputs: 126

list initialization is the only way you can directly initialize a single value of a std::byte object.

std::byte b1{42}; // OK (as for all enums with fixed underlying type since C++17)
std::byte b2(42); // ERROR
std::byte b3 = 42; // ERROR
std::byte b4 = {42}; // ERROR

There is also no implicit conversion, so that you have to initialize the byte array with an explicitly converted integral literal:

std::byte b5[] {1}; // ERROR
std::byte b6[] {std::byte{1}}; // OK

Operations:

OperationEffect
constructorsCreate a byte object (value undefined with default constructor)
destructorDestroys a byte object (nothing to be done)
=Assign a new value
==, !=, <, <=, >, >=Compares byte objects
<<, >>,, &, ^, ~
<<=, >>=,=, &=, ^=
to_integer()Converts byte object to integral type T
sizeof()Yields 1

5. String Views

6. File system

OTHER

Remove

  1. keyword register
  2. The increment operator for bool
  3. Removing Deprecated Exception Specifications
  4. Removing Trigraphs ( ??=, ??(, ??< ... etc ))

Fixes

  1. New auto rules for direct-list-initialisation

    • For a braced-init-list with 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
  1. static_assert With no Message
static_assert(std::is_arithmetic_v<T>, "T must be arithmetic");
static_assert(std::is_arithmetic_v<T>); // no message needed since C++17
  1. Different begin and end Types in Range-Based For Loop

Clarification

  1. Stricter Expression Evaluation Order
  2. Guaranteed Copy Elision
  3. Dynamic Memory Allocation for Over-Aligned Data
  4. Exception Specifications in the Type System