Пять сложных тем для элементов данных в C ++ 20

GuDron

dumpz.ws
Admin
Регистрация
28 Янв 2020
Сообщения
7,564
Реакции
1,435
Credits
24,411

Пять сложных тем для элементов данных в C ++ 20​

five_topics_members.png
Работа с элементами данных и дизайном классов необходима практически для любого проекта на C ++. В этой статье будет изложено пять тем, которые, надеюсь, заинтересуют вас внутренними элементами C ++.

1. Изменение статуса агрегатов Для просмотра ссылки Войди или Зарегистрируйся

Интуитивно простой тип класса или массив следует рассматривать как “агрегированный” тип. Это означает, что мы можем инициализировать его с помощью фигурных скобок{}:
Код:
#include <iostream>
#include <array>
#include <type_traits>
#include <utility>
#include <tuple>

struct Point {
double x {0.0};
double y {0.0};
};

int main() {
std::array<int, 4> numbers { 1, 2, 3, 4 };
std::array statuses { "error", "warning", "ok" };  // CTAD
Point pt { 100.0, 100.0 };
std::pair vals { "hello", 10.5f };
std::tuple pack { 10, true, "important" };

static_assert(std::is_aggregate_v<decltype(numbers)>);
static_assert(std::is_aggregate_v<decltype(statuses)>);
static_assert(std::is_aggregate_v<decltype(pt)>);
// not an aggregate...
static_assert(!std::is_aggregate_v<decltype(vals)>);
static_assert(!std::is_aggregate_v<decltype(pack)>);
}

Для просмотра ссылки Войди или Зарегистрируйся
Но что такое простой тип класса? С годами определение в C ++ немного изменилось.
В настоящее время, начиная с C ++ 20, у нас есть следующее определение:
Из Для просмотра ссылки Войди или Зарегистрируйся:
Агрегат - это массив или класс с
  • нет объявленных пользователем или унаследованных конструкторов,
  • нет частных или защищенных прямых нестатических элементов данных,
  • нет виртуальных функций и
  • нет виртуальных, частных или защищенных базовых классов.
Однако, например, до C ++ 14 нестатические инициализаторы элементов данных (NSDMI или инициализация внутри класса) были запрещены. В C ++ 11 Pointкласс из предыдущего примера не был агрегатом, но это начиная с C ++ 14.
В C ++ 17 включены базовые классы, а также расширенная поддержка фигурных скобок. Теперь вы можете повторно использовать некоторые удобные агрегаты в качестве базовых классов без необходимости написания конструкторов:
Код:
#include <string>
#include <type_traits>

enum class EventType { Err, Warning, Ok};

struct Event {
EventType evt;
};

struct DataEvent : Event {
std::string msg;
};

int main() {
DataEvent hello { EventType::Ok, "hello world"};

static_assert(std::is_aggregate_v<decltype(hello)>);
}

Для просмотра ссылки Войди или Зарегистрируйся
Если вы скомпилируете с std=c++14флагом, вы получите:
Код:
no matching constructor for initialization of 'DataEvent'
DataEvent hello { EventType::Ok, "hello world"};

Запуск на Для просмотра ссылки Войди или Зарегистрируйся
У нас также есть еще несколько незначительных изменений, таких как:
  • конструктор, объявленный пользователем, против определяемого пользователем или явного,
  • унаследованные конструкторы

2. Нет скобок для прямой инициализации и NSDMI Для просмотра ссылки Войди или Зарегистрируйся

Давайте возьмем простой класс с элементом по умолчанию, установленным на `“пустой”:
Код:
class DataPacket {
std::string data_ {"empty"};
// ... the rest...
Что, если я хочу data_, чтобы меня инициализировали с 40 звездочками*? Я могу написать длинную строку или использовать один из std::string конструкторов, использующих счетчик и символ. Тем не менее, из-за конструктора, std::initializer_listв std::stringкотором имеет приоритет, вам необходимо использовать прямую инициализацию с помощью parens для вызова правильной версии::
Код:
#include <iostream>

int main() {
std::string stars(40, '*'); // parens
std::string moreStars{40, '*'}; // <<
std::cout << stars << '\n';
std::cout << moreStars << '\n';
}

Для просмотра ссылки Войди или Зарегистрируйся
Если вы запустите код, вы увидите:
Код:
****************************************
(*

Это потому {40, '*'}, что преобразует 40 в символ ((используя его) ASCI-код) и передает эти два символа, std::initializer_listчтобы создать строку только из двух символов. Проблема в том, что прямая инициализация с помощью скобок (круглых скобок) не будет работать внутри объявления члена класса:
Код:
class DataPacket {
std::string data_ (40, '*'); // syntax error!

/* rest of the code*/
Код не компилируется, и чтобы исправить это, вы можете положиться на инициализацию копирования:
Код:
class DataPacket {
std::string data_ = std::string(40, '*'); // fine

/* rest of the code*/
Это ограничение может быть связано с тем фактом, что синтаксические скобки могут быстро столкнуться с самыми неприятными проблемами синтаксического анализа / синтаксического анализа, что может быть еще хуже для членов класса.

3. Никаких вычетов для NSDMI Для просмотра ссылки Войди или Зарегистрируйся

Вы можете использовать autoдля статических переменных:
Код:
class Type {
static inline auto theMeaningOfLife = 42; // int deduced
};

Однако вы не можете использовать его в качестве нестатического члена класса:
Код:
class Type {
auto myField { 0 }; // error
auto param { 10.5f }; // error
};

Альтернативный синтаксис также терпит неудачу:
Код:
class Type {
auto myField = int { 10 };
};

Аналогично для CTAD (из C ++ 17). это прекрасно работает для staticчленов данных класса:
Код:
class Type {
static inline std::vector ints { 1, 2, 3, 4, 5 }; // deduced vector<int>
};

Однако он не работает как нестатический элемент:
Код:
class Type {
std::vector ints { 1, 2, 3, 4, 5 }; // syntax error!
};

То же самое происходит и с массивами, компилятор не может определить ни количество элементов, ни тип:
Код:
struct Wrapper {
int numbers[] = {1, 2, 3, 4}; // syntax error!
std::array nums { 0.1f, 0.2f, 0.3f }; // error...
};

4. Инициализация списка. Является ли это единообразным? Для просмотра ссылки Войди или Зарегистрируйся

Начиная с C ++ 11, у нас появился новый способ инициализации, называемый инициализацией списка {}. Иногда называется инициализацией фигурных скобок или даже равномерной инициализацией.
Действительно ли это единообразно?
В большинстве мест вы можете использовать его ... и с каждым стандартом C ++ правила менее запутанны ... если у вас нет исключения.
Например:
Код:
int x0 { 78.5f }; // error, narrowing conversion
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 (since C++17)
auto x4 = { 3 }; // decltype(x4) is std::initializer_list<int>
auto x5{ 3 }; // decltype(x5) is int (since C++17)
Кроме того, есть эта известная проблема с вектором:
Код:
std::vector<int> vec1 { 1, 2 }; // holds two values, 1 and 2
std::vector<int> vec2 ( 1, 2 ); // holds one value, 2
Для элементов данных нет autoвычета типов или CTAD, поэтому мы должны указать точный тип элемента. Я думаю, что инициализация списка в этом случае более единообразна и менее проблематична.
Некоторые резюме:
  • Для просмотра ссылки Войди или Зарегистрируйся - это безумие - известная статья, в которой перечислены восемнадцать различных форм инициализации (начиная с C ++ 14).
  • В пункте 7 для эффективного современного C ++ Скотт Мейерс сказал, что “ограниченная инициализация - это наиболее широко используемый синтаксис инициализации, он предотвращает сужение преобразований и невосприимчив к самому неприятному синтаксическому анализу C ++.
  • Николай Джосуттис провел отличную презентацию обо всех угловых случаях: Для просмотра ссылки Войди или Зарегистрируйся, и предлагает использовать {}
  • Основные рекомендации: Для просмотра ссылки Войди или Зарегистрируйся . Исключение: для контейнеров существует традиция использования {...}для списка элементов и (...)размеров. Инициализация переменной, объявленной autoс помощью одного значения, например, {v}, , приводила к удивительным результатам до C ++ 17. Правила C ++ 17 несколько менее удивительны.
  • Только Для просмотра ссылки Войди или Зарегистрируйся - предпочитает старый стиль. Это руководство было обновлено в 2015 году, поэтому многие вещи были обновлены с C ++ 17 и C ++ 20.
  • В Для просмотра ссылки Войди или Зарегистрируйся - Тимур предлагает {} для всех, но если вы хотите быть уверены в вызове конструктора, тогда используйте () . As () выполняет регулярное разрешение перегрузки.
В книге о Для просмотра ссылки Войди или Зарегистрируйся я следую правилу, которое следует использовать {}в большинстве мест, если только это ()не очевидно для вызова какого-либо правильного конструктора.

5. std::initializer_listявляется жадным Для просмотра ссылки Войди или Зарегистрируйся

Все контейнеры из стандартной библиотеки поддерживают конструкторы initializer_list. Например:
Код:
// the vector class:
constexpr vector( std::initializer_list<T> init,
const Allocator& alloc = Allocator() );

// map:
map( std::initializer_list<value_type> init,
const Compare& comp = Compare(),
const Allocator& alloc = Allocator() );


Мы можем создать наш собственный класс и сравнить это поведение:
Код:
#include <iostream>
#include <initializer_list>

struct X {
X(std::initializer_list<int> list)
: count{list.size()} { puts("X(init_list)"); }
X(size_t cnt) : count{cnt} { puts("X(cnt)"); }
X() { puts("X()"); }
size_t count {};
};

int main() {
X x;
std::cout << "x.count = " << x.count << '\n';
X y { 1 };
std::cout << "y.count = " << y.count << '\n';
X z { 1, 2, 3, 4 };
std::cout << "z.count = " << z.count << '\n';
X w ( 3 );
std::cout << "w.count = " << w.count << '\n';
}

Для просмотра ссылки Войди или Зарегистрируйся
XКласс определяет три конструктора, и один из них принимает initializer_list. Если мы запустим программу, вы увидите следующий вывод:
Код:
X()
x.count = 0
X(init_list)
y.count = 1
X(init_list)
z.count = 4
X(cnt)
w.count = 3

Как вы можете видеть, запись X x;вызывает конструктор по умолчанию. Аналогично, если вы пишете X x{};, компилятор не будет вызывать конструктор с пустым списком инициализаторов. Но в других случаях конструктор списка является “жадным” и будет иметь приоритет над обычным конструктором, принимающим один аргумент. Чтобы вызвать точный конструктор, вам нужно использовать прямую инициализацию с помощью скобок().