Главная | Обратная связь | Поможем написать вашу работу!
МегаЛекции

сonstexpr – обобщенные гарантировано константные выражения




Механизм constexpr

· Предоставляет более обобщенный механизм константных выражений

· Позволяет определять константные выражения, используя типы, определенные пользователем

· Гарантирует инициализацию выражений во время компиляции

Давайте рассмотрим следующий пример:

enum Flags { good=0, fail=1, bad=2, eof=4 }; constexpr int operator|(Flags f1, Flags f2) { return Flags(int(f1)|int(f2)); } void f(Flags x) { switch (x) { case bad: /*... */ break; case eof: /*... */ break; case bad|eof: /*... */ break; default: /*... */ break; } }

 

В данном случае constexpr говорит, что функция должна быть настолько простой, чтобы она могла быть вычислена во время компиляции с указанными константными аргументами.

Кроме того, для возможности вычисления выражения во время компиляции, мы хотим иметь возможность требовать вычисления выражений во время компиляции; для этого служит ключевое слово constexpr перед определением переменной (что неявно подразумевает константность этой переменной).

constexpr int x1 = bad|eof; // ОК void f(Flags f3) { // ошибка: невозможно вычислить выражение во время компиляции constexpr int x2 = bad|f3; int x3 = bad|f3; // ОК }

 

Обычно, мы хотим получить гарантированное вычисление на этапе компиляции: глобальных объектов, объектов уровня пространства имен, и зачастую мы хотим, чтобы они располагались в хранилищах только для чтения.

Этот механизм также работает для объектов с достаточно простыми конструкторами и для выражений с этими объектами.

struct Point { int x,y; constexpr Point(int xx, int yy): x(xx), y(yy) { } }; constexpr Point origo(0,0); constexpr int z = origo.x; constexpr Point a[] = {Point(0,0), Point(1,1), Point(2,2) }; constexpr int x = a[1].x; // x равняется 1

 

Обратите, пожалуйста, внимание, что constexpr не является механизмом общего назначения для замены ключевого слова const (и наоборот):

· Основное назначение ключевого слова const заключается в выражении того, что объект не может быть изменен через его интерфейс (хотя объект может быть спокойно изменен через другие интерфейсы). Просто так уж получается, что объявление объекта константным дает отличные возможности компилятору для оптимизации. В частности, если объект объявлен с ключевым словом const и его адрес не вычисляется, компилятор обычно может выполнить его инициализацию во время компиляции (хотя это и не гарантируется) и хранить этот объект в собственных таблицах вместо помещения его в сгенерированный код.

· Основное назначение ключевого слова constexpr заключается в расширении диапазона того, что может быть вычислено во время компиляции, делая эти вычисления типобезопасными. Инициализаторы объектов, объявленные с ключевым словом constexpr, выполняются на этапе компиляции; по сути - это значения, которые хранятся в таблицах компилятора и они попадают в сгенерированный код только при необходимости.

См. также:

  • the C++ draft 3.6.2 Initialization of non-local objects, 3.9 Types [12], 5.19 Constant expressions, 7.1.5 The constexpr specifier
  • [N1521=03-0104] Gabriel Dos Reis: Generalized Constant Expressions (original proposal).
  • [N2235=07-0095] Gabriel Dos Reis, Bjarne Stroustrup, and Jens Maurer: Generalized Constant Expressions -- Revision 5.

 

Decltype – тип выражения

decltype(E) – это тип («объявленный тип», declared type) имени или выражения E,который может быть использован в объявлениях. Например:

void f(const vector<int>& a, vector<float>& b) { typedef decltype(a[0]*b[0]) Tmp; for (int i=0; i<b.size(); ++i) { Tmp* p = new Tmp(a[i]*b[i]); //... } //... }

 

Эта идея, под названием “typeof” была уже давно известна в среде обобщенного (generic) программирования, но реальные реализации typeof всегда были неполными и несовместимыми, поэтому в текущем стандарте было решено использовать новое понятие: decltype.

Если вам просто нужен тип переменной, которую вы хотите проинициализировать, то более простым решением является использование ключевого слова auto. Использование decltype может понадобиться для чего-то другого помимо типа переменных, например, для типов возвращаемого значения.

См. также:

  • the C++ draft 7.1.6.2 Simple type specifiers
  • [Str02] Bjarne Stroustrup. Draft proposal for "typeof". C++ reflector message c++std-ext-5364, October 2002. (original suggestion).
  • [N1478=03-0061] Jaakko Jarvi, Bjarne Stroustrup, Douglas Gregor, and Jeremy Siek: Decltype and auto (original proposal).
  • [N2343=07-0203] Jaakko Jarvi, Bjarne Stroustrup, and Gabriel Dos Reis: Decltype (revision 7): proposed wording.

 

Списки инициализации

Давайте рассмотрим следующий пример:

vector<double> v = { 1, 2, 3.456, 99.99 }; list<pair<string,string>> languages = { {"Nygaard","Simula"}, {"Richards","BCPL"}, {"Ritchie","C"} }; map<vector<string>,vector<int>> years = { { {"Maurice","Vincent", "Wilkes"},{1913, 1945, 1951, 1967, 2000} }, { {"Martin", "Ritchards"} {1982, 2003, 2007} }, { {"David", "John", "Wheeler"}, {1927, 1947, 1951, 2004} } };

 

Теперь списки инициализации могут использоваться не только для массивов. Механизмом доступа к {}-списку является функция (в большинстве случаев конструктор), принимающая в качестве аргумента std::initializer_list<T>. Например:

void f(initializer_list<int>); f({1,2}); f({23,345,4567,56789}); f({}); // пустой список f{1,2}; // ошибка: пропущен вызов метода () years.insert({{"Bjarne","Stroustrup"},{1950, 1975, 1985}});

 

Список инициализации может быть произвольной длины, но должен быть однородным (все элементы должны быть такого же типа, что указан в качестве параметра шаблона (std::initializer_list<T>), или же должны преобразовываться к T).

Контейнер может реализовывать конструктор, принимающий список инициализации следующим образом:

template<class E> class vector { public: // конструктор, принимающий список инициализации vector (std::initializer_list<E> s) { // выделить нужное количество памяти reserve(s.size()); // проинициализировать элементы в диапазонеelem[0:s.size())) uninitialized_copy(s.begin(), s.end(), elem); sz = s.size(); // установить размер вектора } //... как и было... };

 

Различия между непосредственной инициализацией и инициализацией копирования сохраняется и в случае использования списков инициализации, но в этом случае разница менее существенна. Например, у std::vector есть явный (explicit) конструктор, принимающий int и принимающий список инициализации:

// ОК: v1 содержит 7 элементов vector<double> v1(7); // ошибка: отсутствует преобразование из int к vector v1 = 9; // ошибка: отсутствует преобразования из int к vector vector<double> v2 = 9; void f(const vector<double>&); // ошибка: отсутствует преобразование из int к vector f(9); // ОК: v1 содержит 1 элемент (со значением 7) vector<double> v1{7}; // ОК v1 теперь содержит 1 элемент (со значением 9) v1 = {9}; // ОК: v2 содержит 1 элемент (со значением 9) vector<double> v2 = {9}; // ОК: f вызывается со списком { 9 } f({9}); vector<vector<double>> vs = { // ОК: вызов явного конструктора (10 элементов) vector<double>(10), // ОК: вызов явного конструктора (1 элемент со значением 10) vector<double>{10}, // ошибка: конструктор класса vector явный (explicit) 10 };

 

Функция может использовать initializer_list как неизменяемую последовательность. Например:

void f(initializer_list<int> args) { for (auto p=args.begin(); p!=args.end(); ++p) cout << *p << "\n"; }

 

Конструктор, принимающий единственный аргумент типа std::initializer_list называется конструктором со списком инициализации (initializer-list constructor).

Контейнеры стандартной библиотеки, такие как string и regex содержат конструкторы, операторы присваивания и т.д., принимающие списки инициализации. Списки инициализации могут использоваться в качестве диапазонов значений, например, в range-for операторе.

См. также:

  • the C++ draft 8.5.4 List-initialization [dcl.init.list]
  • [N1890=05-0150 ] Bjarne Stroustrup and Gabriel Dos Reis: Initialization and initializers (an overview of initialization-related problems with suggested solutions).
  • [N1919=05-0179] Bjarne Stroustrup and Gabriel Dos Reis: Initializer lists.
  • [N2215=07-0075] Bjarne Stroustrup and Gabriel Dos Reis: Initializer lists (Rev. 3).
  • [N2640=08-0150] Jason Merrill and Daveed Vandevoorde: Initializer Lists -- Alternative Mechanism and Rationale (v. 2) (final proposal).

 

Предотвращение сужения (narrowing) типов

Проблема заключается в том, что языки С и С++ неявно обрезают некоторые типы:

int x = 7.3; // Ой! void f(int); f(7.3); // Ой!

 

Однако списки инициализации С++11 не позволяют сужение (narrowing) типов:

int x0 {7.3}; // ошибка: сужение int x1 = {7.3}; // ошибка: сужение double d = 7; int x2{d}; // ошибка: сужение (double к int) char x3{7}; // ОК: хотя 7 – это int, здесь нет сужения vector<int> vi = { 1, 2.3, 4, 5.6 }; // ошибка: сужение double к int

 

Чтобы избежать большого количества несовместимостей, в С++11 по возможности используется не только анализ типов, но и анализ реальных значений инициализаторов (как в примере с char x3 { 7 }). Сужение не происходит, когда значение может быть в точности представлено целевым типом.

char c1{7}; // ОК: 7 – это int, но он умещается в char char c2{77777}; // ошибка: сужение

 

Обратите внимание, что преобразование из чисел с плавающей запятой к целочисленным типам всегда рассматривается как сужение, даже 7.0 к 7.

См. также:

  • the C++ draft section 8.5.4.
  • [N1890=05-0150 ] Bjarne Stroustrup and Gabriel Dos Reis: Initialization and initializers (an overview of initialization-related problems with suggested solutions).
  • [N2215=07-0075] Bjarne Stroustrup and Gabriel Dos Reis: Initializer lists (Rev. 3).
  • [N2640=08-0150] Jason Merrill and Daveed Vandevoorde: Initializer Lists - Alternative Mechanism and Rationale (v. 2) (primarily on "explicit").

 

Делегирующие конструкторы

В С++98, если два конструктора выполняли одно и то же, то приходилось либо дублировать этот код, либо добавлять функцию инициализации “init”. Например:

class X { int a; void validate(int x) { if (0<x && x<=max) a=x; else throw bad_X(x); } public: X(int x) { validate(x); } X() { validate(42); } X(string s) { int x = lexical_cast<int>(s); validate(x); } //... };

 

Такая многословность ухудшает читабельность, а дублирование может приводить к ошибкам. И все это негативно сказывается на сопровождаемости. Поэтому в C++11 мы можем определить конструктор, в терминах другого конструктора:

class X {

int a;

public:

X(int x) { if (0<x && x<=max) a=x; else throw bad_X(x); }

X():X{42} { }

X(string s):X{lexical_cast<int>(s)} { }

//...

};

 

См. также:

  • the C++ draft section 12.6.2
  • N1986==06-0056 Herb Sutter and Francis Glassborow: Delegating Constructors (revision 3).

 

Инициализация членов класса при объявлении

В С++98 только статические константные члены встроенных типов могли инициализироваться при объявлении. Эти ограничения гарантировали, что инициализация могла быть произведена во время компиляции. Например:

int var = 7;

class X {

// ОК

static const int m1 = 7;

// ошибка: поле не статическое

const int m2 = 7;

// ошибка: поле не константное

static int m3 = 7;

// ошибка: инициализация не константным выражением

static const int m4 = var;

// ошибка: не встроенный тип

static const string m5 = "odd";

//...

};

 

Идея в С++11 заключалась в том, чтобы позволить инициализировать нестатические члены данных в месте объявления (в классе). В случае необходимости, конструктор может использовать инициализатор для инициализации полей во время выполнения. Например, следующий код:

class A {

public:

int a = 7;

};

 

Эквивалентен:

class A {

public:

int a;

A(): a(7) {}

};

 

Это немного экономит время набора, но главное преимущество мы получаем, когда класс содержит несколько конструкторов. Обычно все конструкторы используют одинаковый список инициализации:

class A {

public:

A(): a(7), b(5), hash_algorithm("MD5"), s("Constructor run") {}

A(int a_val): a(a_val), b(5), hash_algorithm("MD5"), s("Constructor run") {}

A(D d): a(7), b(g(d)), hash_algorithm("MD5"), s("Constructor run") {}

int a, b;

private:

// Криптографическая хэш-функция, которая будет применяься

// ко всем экземплярам класса A

HashingFunction hash_algorithm;

// Строка, которая представляет состояние жизненного цикла объекта

std::string s;

};

 

Тот факт, что hash_algorithm и s содержат одинаковые значения по умолчанию, теряются за грудой кода и легко могут привести к проблемам сопровождения. Вместо этого, мы можем разделить процесс инициализации членов:

class A {

public:

A(): a(7), b(5) {}

A(int a_val): a(a_val), b(5) {}

A(D d): a(7), b(g(d)) {}

int a, b;

private:

// Криптографическая хэш-функция, которая будет применяься

// ко всем экземплярам класса A

HashingFunction hash_algorithm{"MD5"};

// Строка, которая представляет состояние жизненного цикла объекта

std::string s{"Constructor run"};

};

 

Если член инициализируется в месте объявления и в списке инициализации, то выполняется только список инициализации (значения по умолчанию «затираются»). Так что предыдущий код мы можем упростить до такого:

class A {

public:

A() {}

A(int a_val): a(a_val) {}

A(D d): b(g(d)) {}

int a = 7;

int b = 5;

private:

// Криптографическая хэш-функция, которая будет применяься

// ко всем экземплярам класса A

HashingFunction hash_algorithm{"MD5"};

// Строка, которая представляет состояние жизненного цикла объекта

std::string s{"Constructor run"};

};

 

См. также:

  • the C++ draft section "one or two words all over the place"; see proposal.
  • [N2628=08-0138] Michael Spertus and Bill Seymour: Non-static data member initializers.

 

Унаследованные конструкторы

Некоторых иногда удивляют правила областей видимости членов класса. В частности то, что члены базового класса не находится в той же области видимости, что и члены наследника:

struct B {

void f(double);

};

struct D: B {

void f(int);

};

B b; b.f(4.5); // Все нормально

D d; d.f(4.5); // Сюрприз: вызываем f(int) с аргументом 4

 

В С++98 мы можем «поднять» набор перегруженных функций из базового класса в наследник:

struct B {

void f(double);

};

struct D: B {

// Добавляем все функции f() из области видимости B

// в текущую область видимости

using B::f;

// Добавляем новую функцию с именем f()

void f(int);

};

B b; b.f(4.5); // Все нормально

D d; d.f(4.5); // Все нормально: вызываем D::f(double),

// которая является B::f(double)

 

Я бы сказал, что лишь историческая случайность не позволяет этой возможности работать с конструкторами так же, как с обычными функциями. С++11 предоставляет такую возможность:

class Derived: public Base {

public:

// Поднимаем функцию f класса Base в область видимости

// класса Derived -- работает в C++98

using Base::f;

// Добавляем новую функцию f

void f(char);

// « Предпочитаем» использовать эту функцию f вместо Base::f(int)

void f(int);

// Поднимаем конструктор Base в область видимости

// класса Derived -- работает только в C++11

using Base::Base;

Derived(char); // Добавляем новый конструктор

Derived(int); // Используем этот конструктор вместо Base::Base(int)

//...

};

 

При наследовании конструктора, вы все еще можете отстрелить себе ногу, если класс наследник содержит дополнительные члены, требующие инициализации:

struct B1 {

B1(int) { }

};

struct D1: B1 {

using B1::B1; // Неявно объявляет D1(int)

int x;

};

Void test()

{

D1 d(6); // Ой: d.x не инициализирован

D1 e; // Ошибка: D1 не содержит конструктор по умолчанию

 

Вы можете защитить свои ноги путем использования инициализаторов членов (member-initializer):

struct D1: B1 {

using B1::B1; // Неявно объявляет D1(int)

int x{0}; // поле x проинициализировано

};

Void test()

{

D1 d(6); // d.x равно 0

}

 

См. также:

  • the C++ draft section 12.9.
  • [N1890=05-0150 ] Bjarne Stroustrup and Gabriel Dos Reis: Initialization and initializers (an overview of initialization-related problems with suggested solutions).
  • [N1898=05-0158 ] Michel Michaud and Michael Wong: Forwarding and inherited constructors.
  • [N2512=08-0022] Alisdair Meredith, Michael Wong, Jens Maurer: Inheriting Constructors (revision 4).

 

Статические утверждения (утверждения времени компиляции) – static_assert

Статические утверждения (утверждения времени компиляции) содержат константное выражение и строковый литерал:

static_assert(expression,string);

 

Компилятор вычисляет выражение, и если результат вычисления равен false (т.е. утверждение нарушено), выводит строку в качестве сообщения об ошибке. Например:

static_assert(sizeof(long)>=8,

"64-bit code generation required for this library.");

struct S { X m1; Y m2; };

static_assert(sizeof(S)==sizeof(X)+sizeof(Y),

"unexpected padding in S");

 

static_assert полезен для явных заданий некоторых утверждений о программе и ее поведении. Обратите внимание, что поскольку выражение static_assert вычисляется во время компиляции, то оно не может применяться для проверки утверждений, зависящих от значений времени выполнения. Например:

int f(int* p, int n)

{

// Ошибка: выражение в static_assert() не является

// константным выражением

static_assert(p==0,"p is not null");

//...

}

 

(вместо этого следует проверить выражение и сгенерировать исключение в случае неудачи).

См. также:

  • the C++ draft 7 [4].
  • [N1381==02-0039] Robert Klarer and John Maddock: Proposal to Add Static Assertions to the Core Language.
  • [N1720==04-0160] Robert Klarer, John Maddock, Beman Dawes, Howard Hinnant: Proposal to Add Static Assertions to the Core Language (Revision 3).

 

Поделиться:





Воспользуйтесь поиском по сайту:



©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...