Jan 23, 2018

[C++17] C++17 - The Complete Guide [note]

C++17 - The Complete Guide


structured bindings do not decay. 

i.e
struct S {
const char x[6];
const char y[3];
};

S s1{};
auto [a, b] = s1; // a and b get the exact member types
the type of a still is const char[6].

This is different from initializing a new object with auto, where types decay:
auto a2 = a; // a2 gets decayed type of a
std::decay
MyStruct ms = { 42, "Jim" };
auto&& [v,n] = std::move(ms);
此時 anonymous entity r-value reference to ms.
v,n r-value reference to anonymous entity.

We can also capture as a forwarding reference using auto&&.
That is, auto&& will resolve to auto& for lvalue references, and auto&& for rvalue references.
MyStruct ms = { 42, "Jim" };
auto [v,n] = std::move(ms);  // new entity with moved-from values from ms

此時 anonymous entity 由 move constructor construct from ms.
也就是 ms 的內容已經 invalidate.

Note that there is only limited usage of inheritance possible.
All non-static data members must be members of the same class definition.
struct B {
int a = 1;
int b = 2;
};

struct D1 : B {
};

auto [x, y] = D1{}; // OK

struct D2 : B {
int c = 3;
};
auto [i, j, k] = D2{}; // Compile-Time ERROR
std::pair
std::tuple
std::array

array:
std::array<int,4> getArray();
auto [i,j,k,l] = getArray();

tuple:
std::tuple<char,float,std::string> getTuple();
auto [a,b,c] = getTuple();
pair:
std::map<std::string, int> coll;
...
auto [pos,ok] = coll.insert({"new",42});
if (!ok) {
// if insert failed, handle error using iterator pos:
...
}
or
if (auto [pos,ok] = coll.insert({"new",42}); !ok) {
// if insert failed, handle error using iterator pos:
const auto& [key,val] = *pos;
std::cout << "already there: " << key << ’\n’;
}


Aggregate Extensions

struct Data {
std::string name;
double value;
};
Data x{"test1", 6.778};


struct MoreData : Data {
bool done;
};
MoreData y{{"test1", 6.778}, false};
struct Data {
const char* name;
double value;
};
struct PData : Data {
bool critical;
void print() const {
std::cout << ’[’ << name << ’,’ << value << "]\n";
}
};
PData y{{"test1", 6.778}, false};
y.print();

PData a{}; // zero-initialize all elements
PData b{{"msg"}}; // same as {{"msg",0.0},false}
PData c{{}, true}; // same as {{nullptr,0.0},true}
PData d; // values of fundamental types are unspecified
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 << s.data; // outputs: ”world”
std::cout << static_cast<std::string>(s); // outputs: ”hello”
std::cout << static_cast<std::complex<float>>(s); // outputs: (4.5,6.7)

Definition of Aggregates

  • 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.

is_aggregate<>
struct Derived;

struct Base {
friend struct Derived;
private:
Base() {
}
};

struct Derived : Base {
};

int main()
{
Derived d1{}; // ERROR since C++17
Derived d2; // still OK (but might not initialize)
}

Before C++17, Derived was not an aggregate. Thus,
Derived d1{};
was calling the implicitly defined default constructor of Derived, which by default called the default constructor of the base class Base.

Although the default constructor of the base class is private, it was valid to be called as via the default constructor of the derived class, because the derived class was defined to be a friend class.

Since C++17, Derived in this example is an aggregate, not having an implicit default constructor
at all (the constructor is not inherited by a using declaration).

So the initialization is an aggregate initialization, for which it is not allowed to call private constructors of bases classes. Whether the base class is a friend doesn't matter.

RVO

http://en.cppreference.com/w/cpp/language/copy_elision

Copy ellision works even copy/move constructor is deleted.
class MyClass
{
public:
...
// no copy/move constructor defined:
MyClass(const MyClass&) = delete;
MyClass(MyClass&&) = delete;
...
};

// Works even no copy constructor
void foo(MyClass param) {}

// Works even no copy constructor
MyClass bar() {
return MyClass();
}

NRVO

Copy/move constructor still needs to be around.

MyClass foo()
{
MyClass obj;
...
return obj; // still requires copy/move support
}
-
MyClass bar(MyClass obj) // copy elision for passed temporaries
{
...
return obj; // still requires copy/move support
}

So, how this benefits us?

Any type that is _not_ copyable can be used in a factory function
which return the type instance, which is not going to be copied!
#include <utility>
template <typename T, typename... Args>
T create(Args&&... args)
{
...
return T{std::forward<Args>(args)...};
}


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);

Also, any type with it's move constructor deleted, as in factory function,
we still can pass a r-value instance back.
class CopyOnly {
public:
CopyOnly() {
}
CopyOnly(int) {
}
CopyOnly(const CopyOnly&) = default;
CopyOnly(CopyOnly&&) = delete; // explicitly deleted
};

CopyOnly ret() {
return CopyOnly{}; // OK since C++17
}
CopyOnly x = 42; // OK since C++17


Value Categories


It's worth emphasizing that strictly speaking glvalues, prvalues, and xvalues are terms for expressions and not for values.

A variable itself is not an lvalue;
only an expression denoting the variable is an lvalue:
int x = 3; // x here is a variable, not an lvalue
int y = x; // x here is an lvalue

In the first statement 3 is a prvalue initializing the variable (not the lvalue) x.
In the second statement x is an lvalue (its evaluation designates an object containing the value 3). The lvalue x is converted to a prvalue, which is what initializes the variable y.

The key approach to explain value categories now is that in general we have two kinds of expressions:
  • glvalues: expressions for locations of objects or functions
  • prvalues: expressions for initializations
An xvalue is then considered a special location, representing an object whose resources can be reused (usually because it is near the end of its lifetime).

C++17 then introduces a new term, called materialization (of a temporary) for the moment a prvalue becomes a temporary object.
Thus, a temporary materialization conversion is a prvalue-to-xvalue conversion.

Lambda
It's constexpr iff it doesn't capture.

auto squared = [](auto val) {
 // implicitly constexpr since C++17
return val*val;
};
std::array<int,squared(5)> a; // OK since C++17 => std::array<int,25>
auto squared2 = [](auto val) {
 // implicitly constexpr since C++17
static int calls = 0;
 // OK, but disables lambda for constexpr contexts
...
return val*val;
};
std::array<int,squared2(5)> a;
 // ERROR: static variable in compile-time context
std::cout << squared2(5) << ’\n’; // OK

如何測試lambda expression 為constexpr? 

將lambda expression 冠上constexpr,如果compile出錯,則非constexpr.

Evaluation Order


To fix all this unexpected behavior, for some operators the evaluation guarantees were refined so
that they now specify a guaranteed evaluation order:
For:
e1 [ e2 ]
e1 . e2
e1 .* e2
e1 ->* e2
e1 << e2
e1 >> e2
e1 is guaranteed to get evaluated before e2 now, so that the evaluation order is left to right.


However, note that the evaluation order of different arguments of the same function call is still undefined. 
That is, in
e1.f(a1,a2,a3)
e1 is guaranteed to get evaluated before a1, a2, and a3 now. However, the evaluation order of a1,
a2, and a3 is still undefined.
In all assignment operators:
e2 = e1
e2 += e1
e2 *= e1
...
the right-hand side e1 is guaranteed to get evaluated before the left-hand side e2 now.
In new expressions like: (重要!)
new Type(e)

the allocation is now guaranteed to be performed before the evaluation e, and the initialization of the new value is guaranteed to happen before any usage of the allocated and initialized value.


Enum Initialization from Integral Values

For enumerations with a fixed underlying type, since C++17 you can use an integral value of that type for direct list initialization. 

This applies to unscoped enumerations with a specified type and all scoped enumerations, because they always have an underlying default type:
unscoped enum with underlying type
enum MyInt : char { };
MyInt i1{42}; // OK since C++17 (ERROR before C++17)
MyInt i2 = 42; // still ERROR
MyInt i3(42); // still ERROR
MyInt i4 = {42}; // still ERROR
scoped enum with default underlying type
enum class Salutation { mr, mrs };
Salutation s1{0}; // OK since C++17 (ERROR before C++17)
Salutation s2 = 0; // still ERROR
Salutation s3(0); // still ERROR
Salutation s4 = {0}; // still ERROR
The same applies if Salutation has a specified underlying type:

scoped enum with specified underlying type
enum class Salutation : char { mr, mrs };
Salutation s1{0}; // OK since C++17 (ERROR before C++17)
Salutation s2 = 0; // still ERROR
Salutation s3(0); // still ERROR
Salutation s4 = {0}; // still ERROR

For unscoped enumerations (enum without class) having no specified underlying type,
you still can't use list initialization for numeric values
enum Flag { bit1=1, bit2=2, bit3=4 };
Flag f1{0}; // still ERROR
Note also that list initialization still doesn't allow narrowing, so you can't pass a floating-point value.
enum MyInt : char { };
MyInt i5{42.2}; // still ERROR


Fixed Direct List Initialization with auto

int x{42}; // initializes an int
int y{1,2,3}; // ERROR
auto a{42}; // initializes an int now
auto b{1,2,3}; // ERROR now

auto c = {42}; // still initializes a std::initializer_list<int>
auto d = {1,2,3}; // still OK: initializes a std::initializer_list<int>

auto a{42}; // initializes an int now
auto c = {42}; // still initializes a std::initializer_list<int>


Since C++17 exception handling specifications became part of the type of a function

void f1();
void f2() noexcept;// different type

void (*fp)() noexcept; // pointer to function that doesn't throw
fp = f2; // OK
fp = f1; // ERROR since C++17

void (*fp2)(); // pointer to function that might throw
fp2 = f2; // OK
fp2 = f1; // OK

It is not allowed to overload a function name for the same signature with a different exception specification (as it is not allowed to overload functions with different return types only):

void f3();
void f3() noexcept; // ERROR
也就是對於template T而言以上兩個function為不同type.
在template programming中,注意noexcept也為type的一部分!

__has_include

#if __has_include(<filesystem>)
# include <filesystem>
# define HAS_FILESYSTEM 1
#elif __has_include(<experimental/filesystem>)
# include <experimental/filesystem>
# define HAS_FILESYSTEM 1
# define FILESYSTEM_IS_EXPERIMENTAL 1
#elif __has_include("filesystem.hpp")
# include "filesystem.hpp"
# define HAS_FILESYSTEM 1
# define FILESYSTEM_IS_EXPERIMENTAL 1
#else
# define HAS_FILESYSTEM 0
#endif


Deduction Guides

We can define specific deduction guides to provide additional or fix existing class template argument deductions. 

e.g:
template<typename T>
struct C {
C(const T&) {
}
...
};

C x{"hello"}; // T deduced as const char[6]

// now we do
template<typename T> C(T) -> C<T>;

C x{"hello"}; // T deduced as const char*
i.e 重要!
A corresponding deduction guide sounds very reasonable for any class template having a constructor taking an object of its template parameter by reference.

Non-Template Deduction Guides

template<typename T>
struct S {
T val;
};

S(const char*) -> S<std::string>; // map S<> for string literals to S<std::string>
Note that aggregates need list initialization
(the deduction works, but the initialization is not allowed):
S s4 = "hello"; // ERROR (can’t initialize aggregates that way)
Deduction guides compete with the constructors of a class.
Class template argument deduction uses the constructor/guide that has the highest priority according to overload resolution.

If a constructor and a deduction guide match equally well, the deduction guide is preferred.


Explicit Deduction Guides

template<typename T>
struct S {
T val;
};
explicit S(const char*) -> S<std::string>;

S s1 = {"hello"}; // ERROR (deduction guide ignored and otherwise invalid)

S s2{"hello"}; // OK, same as: S<std::string> s1{"hello"};
S s3 = S{"hello"}; // OK
S s4 = {S{"hello"}}; // OK
another e.g
template<typename T>
struct Ptr
{
Ptr(T) { std::cout << "Ptr(T)\n"; }
template<typename U>
Ptr(U) { std::cout << "Ptr(U)\n"; }
};
template<typename T>
explicit Ptr(T) -> Ptr<T*>;

Ptr p1{42}; // deduces Ptr<int*> due to deduction guide
Ptr p2 = 42;    // deduces Ptr<int> due to constructor
int i = 42;
Ptr p3{&i};     // deduces Ptr<int**> due to deduction guide
Ptr p4 = &i;    // deduces Ptr<int*> due to constructor


Deduction Guides for Aggregates

template<typename T>
struct A {
T val;
};

A i1{42}; // ERROR
A s1("hi"); // ERROR
A s2{"hi"}; // ERROR
A s3 = "hi"; // ERROR
A s4 = {"hi"}; // ERROR

A(const char*) -> A<std::string>;

A s2{"hi"}; // OK
A s4 = {"hi"}; // OK

Standard Deduction Guides Deduction from Iterators

namespace std {
template<typename Iterator>
vector(Iterator, Iterator)
-> vector<typename iterator_traits<Iterator>::value_type>;
}

std::set<float> s;
std::vector(s.begin(), s.end()); // OK, deduces std::vector<float>


std::array<> Deduction

std::array a{42,45,77}; // OK, deduces std::array<int,3>
namespace std {
template<typename T, typename... U>
array(T, U...)
-> array<enable_if_t<(is_same_v<T,U> && ...), T>,
(1 + sizeof...(U))>;
}


if constexpr
注意,任何type T dependable expression can be failed which is OK.
Expression without T dependable will not compile if the expression is itself invalid.
No Short-Circuit Compile-Time Conditions.


Fold Expressions

template<typename... T>
auto foldSum (T... args) {
return (... + args); // ((arg1 + arg2) + arg3) ...
}

Motivation for Fold Expressions:

Since before, we can't extract Arg... but only to implement function with recursive calls and retract argument one by one.
template<typename T>
const T& spaceBefore(const T& arg) {
std::cout << ’ ’;
return arg;
}

template <typename First, typename... Args>
void print (const First& firstarg, const Args&... args) {
std::cout << firstarg;
(std::cout << ... << spaceBefore(args)) << ’\n’;
}

// std::cout << spaceBefore(arg1) << spaceBefore(arg2) << ...

Supported Operators: 

all binary operators for fold expressions except .
->
[]
Fold expression can also be used for the comma operator, combining multiple expressions into one statement.
// template for variadic number of base classes
template<typename... Bases>
class MultiBase : private Bases...
{
public:
void print() {
// call print() of all base classes:
(... , Bases::print());
}
};


Dealing with Strings as Template Parameters 

Non-type template parameters can be only
  • constant integral values (including enumerations),
  • pointers to objects/functions/members,
  • lvalue references to objects
  • or functions, or std::nullptr_t (the type of nullptr)
For pointers, linkage is required, which means that you can't pass string literals directly.

However, since C++17, you can have pointers with internal linkage.

For example:
template<const char* str>
class Message {
...
};

extern const char hello[] = "Hello World!";     // external linkage
const char hello11[] = "Hello World!";              // internal linkage

void foo()
{
Message<hello>  msg;              // OK (all C++ versions)
Message<hello11> msg11;     // OK since C++11

static const char hello17[] = "Hello World!";       // no linkage

Message<hello17> msg17;         // OK since C++17
}

--
template<int* p> struct A {
};

int num;
A<&num> a;  // OK since C++11


--
int num;

constexpr int* pNum() {
return & num;
}

A<pNum()> b;  // ERROR before C++17, now OK

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.