Pull to refresh

Обзор новых возможностей С++14: Часть 1

Reading time 10 min
Views 138K
В апреле в Бристоле прошла встреча комитета С++, на которой были рассмотрены первые предложения по внесению изменений в новый стандарт С++14. Все рассматриваемые в этой статье изменения были одобрены на этой встрече и уже занимают свое почетное место в последней версии черновика нового стандарта (N3690 от 15 мая 2013).

Краткий перечень:
  • Автоматическое определение типа возвращаемого значения для обычных функций
  • Обобщенная инициализация захваченных переменных лямбд с поддержкой захвата-по-перемещению
  • Обобщенные (полиморфные) лямбда-выражения
  • Упрощенные ограничения на создание constexpr функций
  • Шаблоны переменных
  • exchange
  • make_unique
  • Обособленные строки
  • Пользовательские литералы для типов стандартной библиотеки
  • optional
  • shared_mutex и shared_lock
  • dynarray


Изменения в самом языке


Автоматическое определение типа возвращаемого значения для обычных функций


Начиная с С++11 в языке появилась возможность определять лямбда-выражения, для которых, в случае, если у Вас всего один return оператор, можно не указывать тип возвращаемого значения — компилятор может вывести его самостоятельно. Многие были удивлены тем, что такая возможность отсутствует для обычных функций. Соответствующее предложение было внесено еще до выхода С++11, однако его отложили из-за ограничений по времени, и вот теперь, спустя несколько лет, его добавили в стандарт. Так же, данная поправка говорит о том, что если функция или лямбда-выражение имеют несколько операторов return, возвращающих один и тот же тип, то в данном случае, компилятор тоже должен выводить значение типа автоматически, включая случай с рекурсивным вызовом.
auto iterate(int len)    // возвращаемое значение - int
{
  for (int i = 0; i < len; ++i)
    if (search (i))
      return i;
  return -1;
}

auto h() { return h(); } // ошибка, тип возвращаемого значения не известен

auto sum(int i) {
  if (i == 1)
    return i;           // возвращаемое значение - int
  else
    return sum(i-1)+i;     // теперь можно вызывать рекурсивно
}

template <class T> auto f(T t) { return t; } // тип будет выведен во время инстанцирования
 
[]()->auto& { return f(); }                  // возврат ссылки

Несмотря на то, что данная возможность имеет большой потенциал, она имеет некоторые ограничения:
Если Вы разместите объявление функции в заголовочном файле, а определение в соответствующий файл с исходным кодом, то при подключении заголовочного файла в другие файлы, компилятор не сможет вывести тип:
// foo.h
class Foo {
public:
  auto getA() const;
};

// foo.cpp
#include "foo.h"

auto Foo::getA() const
{
  return something;
}

// main.cpp
#include "foo.h"

int main() {
  Foo bar;
  auto a = bar.getA(); // ошибка, невозможно вывести тип
}

А значит использовать это получится только с локальными функциями или с функциями, определенными в заголовочных файлах. К последним, как правило, относятся шаблонные функции — основное, на мой взгляд, применение для данной новинки.
К ограничениям, определенным в стандарте, относятся: запрет на использование с виртуальными функциями и запрет на возврат объекта типа std::initializer_list.

Обобщенная инициализация захваченных переменных лямбд с поддержкой захвата-по-перемещению


В С++11 лямбды не поддерживают захват-по-перемещению (capture-by-move). Например, следующий код не будет скомпилирован:
#include <memory>
#include <iostream>
#include <utility>

template <class T> void run(T&& runnable)
{
  runnable();
};

int main()
{
  std::unique_ptr<int> result(new int{42});
  run([result](){std::cout << *result << std::endl;});
}

Чтобы переместить объект внутрь лямбда функции, необходимо было написать какую-нибудь обертку, наподобие устаревшего auto_ptr.
Вместо того, чтобы добавить возможность явного захвата-по-перемещению, было принято решение добавить поддержку обобщенной инициализации захваченные переменных. Например,
[ x { move(x) }, y = transform(y, z), foo, bar, baz ] { ... } 

В данном случае, x будет напрямую инициализирован перемещением x, y будет инициализирован результатом вызова transform, а остальные будут захвачены по значению. Другой пример:
int x = 4;
auto y = [&r = x, x = x+1]()->int {
            r += 2;
            return x+2;
         }();  // Обновляет ::x до 6, и инициализирует y 7-кой.

Не запрещено и явное указание типа инициализируемых захваченных переменных:
[int x = get_x()] { ... } 
[Container y{get_container()}] { ... }
[int z = 5] { ... }

У многих может возникнуть вопрос, почему просто не поддерживать следующий способ:
[&&x] { ... } 

Проблема в том, что мы не захватываем по rvalue ссылке, мы пытаемся переместить. Если бы мы пошли таким способом, то перемещение могло бы произойти гораздо позже, когда реальный объект уже может не существовать. Мы должны перемещать объект во время захвата переменных, а не во время вызова лямбды.

Обобщенные (полиморфные) лямбда-выражения


В С++11 лямбда выражения создают объекты класса, имеющего нешаблонный оператор вызова функции. Данная поправка предлагает:
  • Позволить использовать auto тип-спецификатор, означающий обобщенный (шаблонный) лямбда параметр
  • Позволить преобразование из лямбда функции, не захватыющей значения, к соответствующему указателю-на-функцию

Таким образом, одна и та же шаблонная лямбда может использоваться в разных контекстах:
void f1(int (*)(int))   { }
void f2(char (*)(int))  { }
 
void g(int (*)(int))    { }  // #1
void g(char (*)(char))  { }  // #2
 
void h(int (*)(int))    { }   // #3
void h(char (*)(int))   { }   // #4
 
auto glambda = [](auto a) { return a; };
f1(glambda);  // OK
f2(glambda);  // ошибка: ID не конвертируем
g(glambda);   // ошибка: двусмысленно
h(glambda);   // OK: вызывает #3, так как может быть сконвертировано из ID
int& (*fpi)(int*) = [](auto* a) -> auto& { return *a; }; // OK

Не забываем про возможность использования вместе с универсальными ссылками и шаблонами переменного количества аргументов (variadic templates):
auto vglambda = [](auto printer) {
   return [=](auto&& ... ts) {   // OK: ts - это упакованные параметры функции
       printer(std::forward<decltype(ts)>(ts)...);
   };
};
auto p = vglambda( [](auto v1, auto v2, auto v3)    
                       { std::cout << v1 << v2 << v3; } );
p(1, 'a', 3.14);  // OK: выводит 1a3.14


Упрощенные ограничения на создание constexpr функций


Внесенные изменения позволяют в constexpr функциях использовать:
  • Объявление переменных (за исключением static, thread_local и неинициализированных переменных)
  • if и switch (но не goto)
  • for (включая range-based for), while и do-while
  • Изменение состояния объектов, являющихся результатом constexpr вычислений

constexpr int abs(int x) {
  if (x < 0)
    x = -x;
  return x;                     // OK
}

constexpr int first(int n) {
  static int value = n;         // ошибка: статическая переменная
  return value;
}

constexpr int uninit() {
  int a;                        // ошибка: неинициализированная переменная
  return a;
}

constexpr int prev(int x)
  { return --x; }               // OK

constexpr int g(int x, int n) { // OK
  int r = 1;
  while (--n > 0) r *= x;
  return r;
}

В дополнение, было удалено правило, по которому constexpr нестатические функции-члены неявно получали const спецификатор (подробнее об этом здесь).

Шаблоны переменных


Данная возможность позволяет создавать и использовать constexpr шаблоны переменных, для более удобного сочетания с шаблонными алгоритмами (возможно использование не только со встроенными типами, но и с типами, определенными пользователем):
template<typename T>
constexpr T pi = T(3.1415926535897932385);

template<typename T>
T circular_area(T r) {
  return pi<T> * r * r;
}

struct matrix_constants {
  template<typename T>
  using pauli = hermitian_matrix<T, 2>;

  template<typename T>
  constexpr pauli<T> sigma1 = { { 0, 1 }, { 1, 0 } };

  template<typename T>
  constexpr pauli<T> sigma2 = { { 0, -1i }, { 1i, 0 } };

  template<typename T>
  constexpr pauli<T> sigma3 = { { 1, 0 }, { -1, 0 } };
};


Изменения в стандартной библиотеке


exchange


Атомарные (atomic) объекты предоставляют atomic_exchange функцию, которая позволяет назначить объекту новое значение и вернуть его старое значение. Подобная функция может быть полезна и для обычных объектов.
// вероятная реализация
template<typename T, typename U=T>
T exchange(T& obj, U&& new_val) {
  T old_val = std::move(obj);
  obj = std::forward<U>(new_val);
  return old_val;
}

Для притивных типов эта функция делает тоже самое, что и обычная реализация. Для сложных типов эта функция позволяет:
  • Избежать копирования старого объекта, если для этого типа определен конструктор перемещения
  • Принимать любое тип в качестве нового значения, получая преимущество использования любого конвертирующего оператора присваивания
  • Избежать копирования нового объекта, если он временный или был перемещен сюда

Например, реализация std::unique_ptr::reset:
template<typename T, typename D>
void unique_ptr<T, D>::reset(pointer p = pointer()) {
  pointer old = ptr_;
  ptr_ = p;
  if (old)
    deleter_(old);
}

может быть улучшена до:
template<typename T, typename D>
void unique_ptr<T, D>::reset(pointer p = pointer()) {
  if (pointer old = std::exchange(ptr_, p))
    deleter_(old);
}


make_unique


В С++11, вместе с умными указателями, появилась и такая функция, как make_shared. Она позволяет оптимизировать выделение памяти для shared_ptr, а так же повысить безопасность относительно исключений. И хотя аналогичная оптимизация для unique_ptr невозможна, то повышенная безопасность относительно исключений никогда не повредит. Например здесь, в обоих случаях, мы можем получить утечку памяти, если при создании одного объекта бросилось исключение, а второй объект уже создан, но еще не помещен в unique_ptr:
void foo(std::unique_ptr<A> a, std::unique_ptr<B> b);

int main() {
  foo(new A, new B);
  foo(std::unique_ptr<A>{new A}, foo(std::unique_ptr<B>{new B});
}

Чтобы решить эту ситуацию, было решено добавить функцию make_unique. Таким образом С++14 практически полностью (за исключение очень редких случаев) предлагает программистам отказаться от операторов new и delete.
Пример использования:
#include <iostream>
#include <string>
#include <memory>
using namespace std;

void foo(std::unique_ptr<string> a, std::unique_ptr<string> b) {}

int main() {
    cout << *make_unique<int>() << endl;
    cout << *make_unique<int>(1729) << endl;
    cout << "\"" << *make_unique<string>() << "\"" << endl;
    cout << "\"" << *make_unique<string>("meow") << "\"" << endl;
    cout << "\"" << *make_unique<string>(6, 'z') << "\"" << endl;

    auto up = make_unique<int[]>(5);

    for (int i = 0; i < 5; ++i) {
        cout << up[i] << " ";
    }

    cout << endl;

    foo(make_unique<string>(), make_unique<string>()); // утечка памяти невозможна

    auto up1 = make_unique<string[]>("error"); // ошибка
    auto up2 = make_unique<int[]>(10, 20, 30, 40); // ошибка
    auto up3 = make_unique<int[5]>(); // ошибка
    auto up4 = make_unique<int[5]>(11, 22, 33, 44, 55); // ошибка
}
/* Output:
0
1729
""
"meow"
"zzzzzz"
0 0 0 0 0
*/


Обособленные строки


При использовании строк, включающих в себя пробел, с потоками ввода/вывода, можно получить далеко не всегда ожидаемые результаты. Например:
std::stringstream ss;
std::string original = "foolish me";
std::string round_trip;

ss << original;
ss >> round_trip;

std::cout << original;   // вывод: foolish me
std::cout << round_trip; // вывод: foolish

assert(original == round_trip); // assert сработает

Данная поправка, вносит в стандарт функцию, позволяющую корректно обрабатывать подобные ситуации, добавляя двойные кавычки в начале и в конце строки при записи и удаляя их при чтении. Например:
std::stringstream ss;
std::string original = "foolish me";
std::string round_trip;

ss << quoted(original);
ss >> quoted(round_trip);

std::cout << original;     // вывод: foolish me
std::cout << round_trip;   // вывод: foolish me

assert(original == round_trip); // assert не сработает

Если строка уже содержит кавычки в себе, они будут так же обособлены:
std::cout << "She said \"Hi!\"";  // вывод: She said "Hi!"
std::cout << quoted("She said \"Hi!\"");  // вывод: "She said \"Hi!\""

Данная поправка основана на boost аналоге.

Пользовательские литералы для типов стандартной библиотеки


C++11 вводит понятие пользовательских литералов (ПЛ), но ни одного ПЛ не было определенно для стандартной библиотеки, хотя согласно стандарту, имена ПЛ начинающиеся не с символа нижнего подчеркивания, зарезервированы за STL.
Соответствующая поправка добавляет в стандарт следующие пользовательские литералы:
  • Оператор s для basic_string
  • Операторы h, min, s, ms, us, ns для типов, входящих в chrono::duration
Например:
auto mystring = "hello world"s;   // тип std::string
 
auto mytime = 42ns;               // тип chrono::nanoseconds

Пользовательские литералы s не конфликтуют, поскольку они принимают разные типы параметров.

optional


Данная поправка вводит в стандартную библиотеку новый тип, означающий необязательный объект. Возможный способы использования:
  • Возможность указать, какие параметры функций необязательны
  • Возможность использовать null-состояние (без использования обычных (raw) указателей)
  • Возможность ручного контроля времени жизни RAII объектов
  • Возможность пропустить дорогие (стандартные) конструкторы объектов

Примерный способ использования:
optional<int> str2int(string);    // приводит строку к целому числу, если возможно
 
int get_int_form_user()
{
  string s;
 
  for (;;) {
    cin >> s;
    optional<int> o = str2int(s); // 'o' может содержать целое число, а может и нет
    if (o) {                      // содержит ли 'o' целое число?
      return *o;                  // используем число
    }
  }
}

Данная поправка основана на boost аналоге.
Перевод статьи автора этой поправки об этом классе можно найти здесь.

shared_mutex и shared_lock


При разработке многопоточных программ иногда появляется необходимость дать к некоторому объекту множественный доступ на чтение или уникальный доступ на запись. Данная поправка добавляет в стандартную библиотеку shared_mutex, предназначенный для этой цели. Функция lock дает уникальный доступ, и может быть использона с добавленными ранее lock_guard и unique_lock. Чтобы получить совместный доступ, необходимо использовать lock_shared функцию, и лучше это делать через добавленый RAII класс shared_lock:
using namespace std;
shared_mutex rwmutex;
{
  shared_lock<shared_mutex> read_lock(rwmutex);
  // чтение
}
{
  unique_lock<shared_mutex> write_lock(rwmutex); // или lock_guard
  // запись
}

Стоит отметить, что стоимость любого лока, даже на чтение, дороже, чем при использовании обычного мьютекса.
Данная поправка основана на boost аналоге.

dynarray


В текущем стандарте С есть динамические массивы, размер которых может быть определен только во время выполнения. Как и у обычных массивов, данные у них создаются на стэке. С++14 утверждает такие массивы, но при этом определяет еще и свой собственный вид динамический массив — std::dynarray, который так же узнает свой размер во время выполнения и не может изменять его в дальнейшем, однако может либо расположить объекты на стэке, либо разместить их в куче, при этом имея интерфейс во многом схожий с std::array и std::vector. Согласно поправке, этот класс может быть явно оптимизирован компилятором, для случаев использования стэка.

Заключение


В основном, С++14 позиционируется, как minor bug-fix release, исправляя недостатки С++11, допущенные из-за ограничений по времени или по каким-либо другим причинам. Текущий черновик стандарта С++ можно найти здесь.

UPDATE:
dynarray и optional были вынесены в отдельные технические спецификации.
Обзор новых возможностей С++14: Часть 2
Tags:
Hubs:
+64
Comments 86
Comments Comments 86

Articles