C++17 - poprawki standardu¶
Zdefiniowana kolejność ewaluacji wyrażeń¶
Kolejność ewaluacji wyrażeń dla wielu operatorów w C++ była niezdefiniowana w standardzie. W efekcie przewidzenie wyniku wykonania operacji korzystającej z takich operatorów było w zasadzie niemożliwe.
Oto kilka przykładów:
std::map<int, int> dict;
dict[0] = dict.size(); // after statement: dict = { {0, 0} } or { { 0, 1} }
std::string s = "but I have heard it works even if you don’t believe in it";
s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" don’t"), 6, "");
assert(s == "I have heard it works only if you believe in it"); // it may fail
std::cout << f() << g() << h(); // UB: undefined evaluation of order
std::cout.operator<<(f()).operator<<(g()).operator<<(h());
Reguły ewaluacji wyrażeń w C++17¶
C++17 definiuje następujące reguły ewaluacji:
- Wyrażenia postfix są ewaluowane od lewej do prawej. Dotyczy to również wywołań funkcji i wyrażeń związanych z wyborem składowej klasy (struktury).
- Wyrażenia przypisania są ewaluowane od prawej do lewej.
- Operandy dla operatorów przesunięć są ewaluowane od lewej do prawej.
W rezultacie następujące wyrażenia są ewaluowane w kolejności a, potem b, potem c a następnie d:
a.b
a->b
a->*b
a(b1, b2, b3)
b @= a
a[b]
a << b
a >> b
Dodatkowo wprowadzono regułę, że kolejność ewaluacji wyrażeń zawierających przeciążone operatory jest określona tak jak w przypadku odpowiednich operatorów wbudowanych, a nie przez reguły związane z wywołaniami funkcji.
W rezultacie wyrażenie:
s.replace(0, 4, "") // 1-st
.replace(s.find("even"), 4, "only") // 2nd
.replace(s.find(" don’t"), 6, ""); // 3rd
jest ewaluowane w określony sposób, ale kolejność ewaluacji argumentów wywołań metody replace()
wciąż nie jest specyfikowana przez standard.
Inicjalizacja typów wyliczeniowych¶
Dla typów wyliczeniowych z określonym typem całkowitym (fixed underlying type) standard C++17 dopuszcza
bezpośrednią inicjalizację listową (direct list initialization) wartością całkowitą. Dotyczy to zarówno klasycznych wyliczeń enum
, jak i
wprowadzonych w C++11 wyliczeń enum class
.
Dla klasycznych typów wyliczeniowych enum
bez określonego typu całkowitego taka inicjalizacja wciąż jest traktowana jako błąd kompilacji.
enum class Coffee { espresso, cappucino, dopio };
Coffee c1 = 0; // ERROR (all versions)
Coffee c2(0); // ERROR (all versions)
Coffee c3{0}; // OK since C++17
enum class GuitarType : char { stratocaster, les_paul };
GuitarType gt1 = 1; // ERROR (all versions)
GuitarType gt2(1); // ERROR (all versions)
GuitarType gt3{1}; // OK since C++17
enum EngineType { diesel, petrol, wankel };
// EngineType e1 = 0; // ERROR (all versions)
// EngineType e2(0); // ERROR (all versions)
// EngineType e3{2}; // ERROR (all versions)
enum MovieFormat : char { divx, mpeg };
// MovieFormat mv1 = 1; // ERROR (all versions)
// MovieFormat mv2(1); // ERROR (all versions)
MovieFormat mv3{1}; // OK since C++17
Definiowanie nowych typów całkowitych za pomocą wyliczeń¶
W C++17 można użyć typu wyliczeniowego do zdefiniowania nowego typu całkowitego niepodlegającego niejawnym konwersjom.
enum length_t : size_t {}; // new distinct integral type with some restrictions
length_t x; // OK
length_t x1(9); // ERROR
length_t x2{42}; // OK
length_t x3{-19}; // ERROR (narrowing)
length_t x4 = 665; // ERROR
length_t x5 = length_t(665); // OK
length_t x6 = static_cast<length_t>(665); // OK
x = 42; // ERROR
x = length_t(42); // OK
x = x2; // OK
if (x == x2) // OK
{
int a = x; // OK for enum but ERROR for enum class
cout << x << endl; // OK for enum but ERROR for enum class
cout << x + x << endl; // OK for enum but ERROR for enum class
x2 = x + x; // ERROR
}
Inicjalizacja listowa i auto¶
W C++17 zmieniona została reguła dotycząca automatycznej detekcji typu w przypadku inicjalizacji bezpośredniej za pomocą inicjalizacji listowej.
W C++11/14:
int x1(42); // direct initialization with C++98/03 syntax
int x2{42}; // direct initialization with C++11
int x3 = 665; // copy initialization
auto a1(42); // direct initialization -> int
auto a2{42}; // direct initialization -> initializer_list<int>
auto a3{42, 665}; // direct initialization -> initializer_list<int>
auto a4 = 42; // copy initialization -> int
auto a5 = {42}; // copy initialization -> initializer_list<int>
auto a6 = {42, 665}; // copy initialization -> initializer_list<int>
Po zmianie reguły w C++17:
auto a1(42); // direct initialization -> int
auto a2{42}; // direct initialization -> int (new rule!!!)
auto a3{42, 665}; // ERROR
auto a4 = 42; // copy initialization -> int
auto a5 = {42}; // copy initialization -> initializer_list<int>
auto a6 = {42, 665}; // copy initialization -> initializer_list<int>
Zalety inicjalizacji bezpośredniej z {}¶
- działa z każdym typem
- również z typami wyliczeniowymi
- również z agregatami posiadającymi klasy bazowe
- wykrywane są konwersje zawężające (inicjalizacja zmiennej
int
wartością typufloat
) - bezpośrednia inicjalizacja w połączeniu z mechanizmem
auto
działa prawidłowo od C++17
noexcept jako część typu funkcji¶
System typów w C++17 uwzględnia specyfikację noexcept
dla funkcji.
void func1();
void func2() noexept;
static_assert(is_same_v<decltype(func1), decltype(func2)>); // ERROR - different types
void (*fp)() noexept;
fp = func2(); // OK
fp = func1(); // ERROR since C++17
Zmiana ta może spowodować, że kod z C++14 może się nie skompilować w C++17:
template <typename F>
void call(F f1, F f2)
{
f1();
f2();
}
call(func1, func2); // ERROR since C++17
Elementy usunięte ze standardu¶
Trigrafy
Operator ++ dla typu
bool
Słowo kluczowe
register
Specyfikacja rzucanych wyjątków z listą typów
void foo() throw(std::bad_alloc); // invalid since C++17 void foo() throw(); // OK - but no stack unwinding guarantee