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

Thread-local storage (thread_local)




Простите, пока не было времени написать этот раздел.

См.:

  • [N2659 = 08-0169] Lawrence Crowl: Thread-Local Storage (Final proposal).

 

Юникодные символы

Простите, пока не было времени написать этот раздел.

 

Копирование и повторная генерация исключений

Как вы перехватываете исключение и генерируете его повторно в другом потоке? Воспользуйтесь небольшой магией, описанной в разделе 18.8.5 Exception Propagation:

· exception_ptr current_exception(); Возвращает: объект exception_ptr, содержащий текущее обработанное исключение (15.3) или копию текущего обработанного исключения, или возвращает null, если никакое исключение не было обработано. Ссылающийся объект должен оставаться валидным как минимум до тех пор, пока объект exception_ptr на него ссылается....

· void rethrow_exception(exception_ptr p);

· template<class E> exception_ptr copy_exception(E e); По сути является следующим:

try {

throw e;

} catch(...) {

return current_exception();

}

Это особенно полезно при передаче исключения из одного потока в другой.

 

Extern templates

Спецификация шаблона может явно запретить возможность множественного инстанцирования. Например:

#include "MyVector.h"

// Предотвращаем неявное инстанцирование --

// MyVector<int> будет явно инстанцирован где-то в другом месте

extern template class MyVector<int>;

void foo(MyVector<int>& v)

{

// использование вектора

}

 

«Где-то в другом месте» может быть такой код:

#include "MyVector.h"

// Делаем MyVector доступным клиентам (например, общей библиотеке)

template class MyVector<int>;

 

По сути, это способ избежать значительного количества ненужной работы компилятору и линковщику.

См.:

  • Standard 14.7.2 Explicit instantiation
  • [N1448==03-0031] Mat Marcus and Gabriel Dos Reis: Controlling Implicit Template Instantiation.

 

Inline namespace

Inline namespace – это механизм, обеспечивающий поддержку развития библиотек, путем предоставления механизма версионирования. Рассмотрим пример:

// file V99.h:

inline namespace V99 {

void f(int); // лучше, нежели версия V98

void f(double); // новая возможность

//...

}

// file V98.h:

namespace V98 {

void f(int); // что-то делает

//...

}

// file Mine.h:

namespace Mine {

#include "V99.h"

#include "V98.h"

}

 

Суть в том, что спецификатор inline заставляет объявления во вложенном пространстве имен вести себя так, как будто они объявлены в текущем.

Это поведение очень «статичное» и ориентировано на разработчика библиотеки, поскольку спецификатор inline указывается поставщиком пространства имен, принимая при этом решение за пользователя. Пользователь Mine никак не может указать, что он хочет по умолчанию использовать V98 вместо V99.

См.:

  • Standard 7.3.1 Namespace definition [7]-[9].

 

Операторы явного преобразования

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

// "обычный конструктор" определяет неявное преобразование

struct S { S(int); };

S s1(1); // ok

S s2 = 1; // ok

void f(S);

f(1); // ok (но это может привести к неприятным

// сюрпризам, что если S – это вектор?)

struct E { explicit E(int); }; // явный конструктор

E e1(1); // ok

E e2 = 1; // ошибка (хотя это обычно является сюрпризом)

void f(E);

f(1); // ошибка (защищает от сюрпризов – например,

// конструктор std::vector, принимающий int является явным)

 

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

struct S { S(int) { } /*... */ };

struct SS {

int m;

SS(int x):m(x) { }

// поскольку S не содержит конструктор S(SS)

operator S() { return S(m); }

};

SS ss(1);

S s1 = ss; // ok; аналогично неявному конструктору

S s2(ss); // ok; аналогично неявному конструктору

void f(S);

f(ss); // ok; аналогично явному конструктору

 

К сожалению, не существует явных (explicit) операторов преобразования (поскольку количество примеров, когда это приводит к неприятностям, значительно меньше). В С++11 этот недосмотр учтен и явные операторы преобразования добавлены в язык. Например:

struct S { S(int) { } };

struct SS {

int m;

SS(int x):m(x) { }

// поскольку S не содержит конструктор S(SS)

explicit operator S() { return S(m); }

};

SS ss(1);

S s1 = ss; // ошибка; как и в случае явного конструктора

S s2(ss); // ok; как и в случае явного конструктора

void f(S);

f(ss); // ошибка; как и в случае явного конструктора

 

См. также:

  • Standard: 12.3 Conversions
  • [N2333=07-0193] Lois Goldthwaite, Michael Wong, and Jens Maurer: Explicit Conversion Operator (Revision 1).

 

Улучшения алгоритмов

Алгоритмы стандартной библиотеки частично улучшены за счет добавления новых алгоритмов, частично за счет улучшения существующих алгоритмов, которые стали возможными благодаря новым языковым возможностям, а частично за счет того, что новые языковые возможности позволили проще их использовать:

  • Новые алгоритмы:

bool all_of(Iter first, Iter last, Pred pred);

bool any_of(Iter first, Iter last, Pred pred);

bool none_of(Iter first, Iter last, Pred pred);

Iter find_if_not(Iter first, Iter last, Pred pred);

OutIter copy_if(InIter first, InIter last, OutIter result, Pred pred);

OutIter copy_n(InIter first, InIter::difference_type n, OutIter result);

OutIter move(InIter first, InIter last, OutIter result);

OutIter move_backward(InIter first, InIter last, OutIter result);

pair<OutIter1, OutIter2> partition_copy(InIter first, InIter last, OutIter1 out_true, OutIter2 out_false, Pred pred);

Iter partition_point(Iter first, Iter last, Pred pred);

RAIter partial_sort_copy(InIter first, InIter last, RAIter result_first, RAIter result_last);

RAIter partial_sort_copy(InIter first, InIter last, RAIter result_first, RAIter result_last, Compare comp);

bool is_sorted(Iter first, Iter last);

bool is_sorted(Iter first, Iter last, Compare comp);

Iter is_sorted_until(Iter first, Iter last);

Iter is_sorted_until(Iter first, Iter last, Compare comp);

bool is_heap(Iter first, Iter last);

bool is_heap(Iter first, Iter last, Compare comp);

Iter is_heap_until(Iter first, Iter last);

Iter is_heap_until(Iter first, Iter last, Compare comp);

T min(initializer_list<T> t);

T min(initializer_list<T> t, Compare comp);

T max(initializer_list<T> t);

T max(initializer_list<T> t, Compare comp);

pair<const T&, const T&> minmax(const T& a, const T& b);

pair<const T&, const T&> minmax(const T& a, const T& b, Compare comp);

pair<const T&, const T&> minmax(initializer_list<T> t);

pair<const T&, const T&> minmax(initializer_list<T> t, Compare comp);

pair<Iter, Iter> minmax_element(Iter first, Iter last);

pair<Iter, Iter> minmax_element(Iter first, Iter last, Compare comp);

// Для каждого элемента, на который ссылается итератор i

// в диапазоне [first,last), присваивает *i = value и

// увеличивает value путем вызова ++value

void iota(Iter first, Iter last, T value);

 

  • Результат использования перемещения: перемещение (moving) может быть более эффективным, чем копирование (см. Семантика перемещения). Например, std::sort() и std::st::insert(), основанные на перемещении до 15 раз быстрее аналогичных версий, использующих копирование. На самом деле влияние не такое существенное, как кажется, поскольку операции для таких стандартных типов, как string и vector, обычно оптимизированы для использования перемещения, путем замены стандартных функций swap на специализированные версии, использующих перемещение. Однако, если ваш тип содержит операторы перемещения, вы автоматически получите повышение производительности при использовании стандартных алгоритмов.
    Обратите также внимание на то, что перемещение позволяет простое и эффективное использование сортировки (и другие алгоритмы) для контейнеров с «умными» указателями, в частности, с unique_ptr:

// сравнивает разыменованные (*P) значения

template<class P> struct Cmp<P> {

bool operator() (P a, P b) const { return *a<*b; }

}

vector<std::unique_ptr<Big>> vb;

// заполняетvb unique_ptr-ами объектов Big

// не пытайтесь делать это с auto_ptr

sort(vb.begin(),vb.end(),Cmp<Big>());

 

  • Использование лямбда-выражений: Многие годы люди жаловались на то, что им приходилось писать функции или (лучше) функторы для использования в таких операциях, как Cmp<T> для алгоритмов стандартной библиотеки (и других алгоритмов). Это было особенно болезненным в случае больших функций (которые, кстати, писать не нужно), поскольку в С++98 нельзя определить локальный функтор для использования его в качестве аргумента; сейчас вы это сделать можете. Лямбда-выражения позволяют создавать операторы «на лету».

Sort(vb.begin(),vb.end(),

[](unique_ptr<Big> a, unique_ptr<Big> b) { return *a<*b; });


Я думаю, что поначалу лямбда-выражениями, как и любыми другими мощными новыми возможностями, будут злоупотреблять.

  • Использование списков инициализации. Иногда списки инициализаторов могут быть очень удобными для использования в качестве аргументов. Например, у нас есть несколько строковых переменных и предикат Nocase, для сравнения строк без учета регистра:

auto x = max({x,y,z}, Nocase());

 

См. также:

  • 25 Algorithms library [algorithms]
  • 26.7 Generalized numeric operations [numeric.ops]
  • Howard E. Hinnant, Peter Dimov, and Dave Abrahams: A Proposal to Add Move Semantics Support to the C++ Language. N1377=02-0035.

 

Улучшения контейнеров

Что же случилось со стандартными контейнерами, после появления новых языковых возможностей и десяти лет дополнительного опыта? Прежде всего, у нас появилось несколько новых контейнеров: array (контейнер фиксированного размера), forward_list (односвязный список) и неупорядоченные контейнеры (хеш-таблицы). Потом, контейнеры начали использовать новые языковые возможности, такие как списки инициализаторов, rvalue ссылки, шаблоны с переменным числом аргументов и constexpr. Давайте в качестве примера рассмотрим std::vector.

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

vector<string> vs = { "Hello", ", ", "World!", "\n" };

for (auto s: vs) cout << s;

  • Операторы перемещения. Контейнеры теперь содержат конструктор и оператор перемещения (в дополнение к существующим операциям копирования). Наиболее важным следствием из этого, является возможность эффективного возвращения из функций контейнеров:

vector<int> make_random(int n)

{

vector<int> ref(n);

// некоторый генератор случайных чисел

for(auto& x: ref) x = rand_int(0,255);

return ref;

}

vector<int> v = make_random(10000);

for (auto x: make_random(1000000)) cout << x << '\n';

Смысл здесь заключается в том, что вектор не копируется. Перепишите этот код с выделением вектора в куче и вы столкнетесь с проблемами управления памятью. Перепишите этот код так, чтобы вектор передавался в качестве аргумента в функцию make_random и вы получите значительно менее понятный код (в котором, к тому же, легче допустить ошибку).

  • Улучшенные операции добавления элементов. Моей любимой операцией контейнера является push_back(), которая позволяет элегантно увеличивать размер контейнеру:

vector<pair<string,int>> vp;

string s;

int i;

while(cin>>s>>i) vp.push_back({s,i});

 

В этом коде будет создан объект pair<string,int>, по значениям s и i, который затем будет перемещен в vp. Обратите внимание, он будет не «скопирован», а «перемещен». Существует перегруженная версия метода push_back, которая принимает в качестве аргумента rvalue ссылку, что позволяет использовать конструктор перемещения класса string. Также обратите внимание на использование унифицированного синтаксиса инициализации, что приводит к более короткому коду.

  • Устанавливающие (emplace) операции. Метод push_back() использует конструктор перемещения, который значительно более эффективен по сравнению со стандартными операциями копирования, но мы можем пойти еще дальше. Зачем вообще что-то копировать/перемещать? Почему бы не выделить в векторе нужный объем памяти и просто не создать нужное значение прямо в этой памяти? Такие операции называются “emplace” (т.е. «создающие на месте»). Давайте в качестве примера рассмотрим emplace_back():

vector<pair<string,int>> vp;

string s;

int i;

while(cin>>s>>i) vp.emplace_back(s,i);

 

  • Устанавливающие (emplace) методы принимают шаблонный параметр с переменным числом аргументов (variadic template), который используется для создания требуемого типа. Является ли метод emplace_back() действительно более эффективным по сравнению с методом push_back() зависит от используемого типа и реализации (библиотеки и шаблонов с переменным числом аргументов). Если вы считаете, что эта разница может быть существенной, то измерьте ее. В противном случае отталкивайтесь от эстетических соображений и выбирайте между vp.push_back({s,i}) и vp.emplace_back(s,i); по вашему вкусу. Сейчас я предпочитаю использовать push_back(), но я могу изменить свою точку зрения в будущем.
  • Аллокаторы области видимости (scoped allocators). Контейнеры теперь могут хранить «настоящие объекты-аллокаторы (с состоянием)», и использовать их для управления вложенными выделениями памяти (например, для выделения памяти под элементы контейнера).

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

  • Вычисления времени компиляции (compile-time evaluation): constexpr используется для гарантированного вычисления выражений во время компиляции для bitset, duration, char_traits, complex, array, для атомарных типов, случайных чисел и т.д.
  • Кортежи (tuples): Кортежи были бы невозможны без шаблонов с переменным числом аргументов.

 

Аллокаторы с дополнительным состоянием

Для простоты и компактности контейнеров С++98 не требует от них поддержки аллокаторов с состоянием. Аллокаторы не должны были хранится в объектах контейнеров. Такое поведение все еще используется по умолчанию в С++11, но появилась возможность использования аллокаторов с состоянием, например, можно использовать аллокатор, содержащий указатель на область памяти из которой происходит выделение. Например:

// Вариант для C++98не содержит данных

template<class T> class Simple_alloc {

// обычная реализация аллокатора

};

class Arena {

void* p;

int s;

public:

Arena(void* pp, int ss);

// выделяет из диапазона p[0..ss-1]

};

template<class T> struct My_alloc {

Arena& a;

My_alloc(Arena& aa): a(aa) { }

// обычная реализация аллокатора

};

Arena my_arena1(new char[100000],100000);

Arena my_arena2(new char[1000000],1000000);

// память выделяется аллокатором по умолчанию

vector<int> v0;

// выделяет из my_arena1

vector<int,My_alloc<int>> v1(My_alloc<int>{my_arena1});

// выделяет из my_arena2

vector<int,My_alloc<int>> v2(My_alloc<int>{my_arena2});

// выделяет с помощью Simple_alloc

vector<int,Simple_alloc<int>> v3;

 

Обычно, для облегчения синтаксиса используют typedef.

Нет никакой гарантии того, что Simple_alloc и аллокатор, используемый по умолчанию столкнутся с нехваткой памяти, но это можно гарантировать с помощью небольшого количества метапрограммирования шаблонов. Таким образом, использование аллокатора приводит к дополнительному расходу памяти только в том случае, когда аллокатор обладает состоянием (как My_alloc).

При использовании пользовательских аллокаторов с контейнерами может возникнуть одна коварная проблема: должен ли элемент располагаться в той же самой выделенной области, что и контейнер? Например, если вы используете Your_allocator для Your_string для выделения его элементов, и я использую My_allocator для выделения элементов в объекте My_vector, тогда какой аллокатор должен использоваться для элементов в типе My_vector<Your_allocator>>? Решение заключается в возможности указать контейнеру какой аллокатор должен передаваться его элементам. Например, предположим, что у меня есть аллокатор My_alloc и я хочу, чтобы vector<string> использовал My_alloc дляэлементов вектора и для выделения элементов строки. Прежде всего, нужно создать версию строки с My_alloc.

// строка с нужным аллокатором

using xstring = basic_string<char,

char_traits<char>, My_alloc<char>>;

 

Затем, мне нужно создать версию vector, принимающую строки и My_alloc, и передающий этот аллокатор строке:

using svec = vector<xstring,

scoped_allocator_adaptor<My_alloc<xstring>>>;

 

Теперь мы можем создать аллокатор типа My_alloc<xstring>:

svec v(svec::allocator_type(My_alloc<xstring>{my_arena1}));

 

Теперь svec – это вектор строк, который использует My_alloc для выделения памяти для своих элементов. Новое поведение заключается в использовании «адаптера» (“wrapper”) из стандартной библиотеки под названием scoped_allocator_adaptor, который указывает на то, что строки также должны использовать My_alloc. Обратите внимание, что адаптер может (очень легко) преобразовывать My_alloc<xstring>, требуемый типу xstring.

Так что у нас есть 4 варианта:

// vector и string используют свой собственный

// аллокатор (аллокатор по умолчанию):

using svec0 = vector<string>;

svec0 v0;

// vector (только) использует My_alloc, а string свой

// собственный аллокатор (аллокатор по умолчанию):

using svec1 = vector<string,My_alloc<string>>;

svec1 v1(My_alloc<string>{my_arena1});

// vector и string используют My_alloc:

using xstring = basic_string<char, char_traits<char>, My_alloc<char>>;

using svec2 = vector<xstring,scoped_allocator_adaptor<My_alloc<xstring>>>;

svec2 v2(scoped_allocator_adaptor<My_alloc<xstring>>{my_arena1});

// vector использует My_alloc и string использует My_string_alloc:

using xstring2 = basic_string<char, char_traits<char>, My_string_alloc<char>>;

using svec3 = vector<xstring2,scoped_allocator_adaptor<My_alloc<xstring>, My_string_alloc<char>>>;

svec3 v3(scoped_allocator_adaptor<My_alloc<xstring2>, My_string_alloc<char>>{my_arena1,my_string_arena});

 

Очевидно, что первая версия, svec0, будет использоваться наиболее часто, но для систем с серьезными ограничениями памяти другие версии (особенно svec2) могут быть очень важны. Использование нескольких typedef немного повысят читабельность кода, но к счастью, это не тот код, который вам придется писать каждый день. scoped_allocator_adapter2 является разновидностью scoped_allocator_adapter для случаев, когда используются разные кастомные аллокаторы.

См. также:

  • Standard: 20.8.5 Scoped allocator adaptor [allocator.adaptor]
  • Pablo Halpern: The Scoped Allocator Model (Rev 2). N2554=08-0064.

 

Std::array

array – это стандартный контейнер, определенный в <array>, который представляет собой последовательность фиксированного размера с возможностью случайного доступа к элементам. У него нет дополнительного расхода памяти для хранения элементов, память для элементов выделяется в куче, он может инициализироваться с помощью списка инициализации, знает свой размер (количество элементов), и не может быть неявным образом преобразован к указателю. Другими словами, этот класс очень похож на стандартный массив, но без его недостатков.

array<int,6> a = { 1, 2, 3 };

a[3]=4;

// x равен 0 поскольку элементы по умолчанию

// инициализируются нулями

int x = a[5];

// ошибка: std::array не может быть неявно преобразован к указателю

int* p1 = a;

// ok: получаем указатель на первый элемент

int* p2 = a.data();

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

// ошибка: размер неизвестен/отсутствует

array<int> a3 = { 1, 2, 3 };

// ok: пустой массив

array<int,0> a0;

// поведение не определено; не делайте этого

int* p = a0.data();

 

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

template<class C> C::value_type sum(const C& a)

{

return accumulate(a.begin(),a.end(),0);

}

array<int,10> a10;

array<double,1000> a1000;

vector<int> v;

//...

int x1 = sum(a10);

int x2 = sum(a1000);

int x3 = sum(v);

 

Кроме того, вы не получите (потенциально опасного) преобразования наследников к базовому классу:

struct Apple: Fruit { /*... */ };

struct Pear: Fruit { /*... */ };

void nasty(array<Fruit*,10>& f)

{

f[7] = new Pear();

};

array<Apple*,10> apples;

//...

// ошибка: невозможно преобразовать array<Apple*,10> к array<Fruit*,10>;

nasty(apples);

 

Если бы это было возможным, то в переменной apples содержались бы экземпляры Pear.

См. также:

  • Standard: 23.3.1 Class template array

 

Std::forward_list

Стандартный контейнер forward_list определен в файле <forward_list> и является по сути односвязным списком. Он поддерживает итерирование только вперед и гарантирует, что элемент не будет перемещен после вставки или удаления другого элемента. Этот класс расходует минимальный объем памяти (пустой список будет занимать скорее всего одно слово) и не предоставляет метод size() (поэтому ему не нужно содержать дополнительное поле с размером):

template <ValueType T, Allocator Alloc = allocator<T> >

requires NothrowDestructible<T>

class forward_list {

public:

// обычные члены контейнеров

// нет метода size()

// нет возможности итерироваться в обратном направлении

// нет методовback() или push_back()

};

 

См. также:

  • Standard: 23.3.3 Class template forward_list

 

Неупорядоченные контейнеры

Неупорядоченные контейнеры являются разновидностью хеш-таблиц. В С++11 представлены следующие типы:

  • unordered_map
  • unordered_set
  • unordered_multimap
  • unordered_multiset

Эти классы должны называться hash_map и т.п., но поскольку этими именами очень часто злоупотребляют, то при выборе новых имен комитет решил остановиться на unordered_map и т.д. как на наименьшем зле. “unordered” в названии классов говорит об отличиях между map и unordered_map: перебор элементов объекта типа map происходит в порядке, зависящем от оператора сравнения (по умолчанию используется <), в то время как значения в unordered_map не должны содержать оператор сравнения и хеш-таблица по своей природе не упорядочена. И наоборот, элементам, хранящимся в map не требуется хеш-функция.

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

map<string,int> m {

{"Dijkstra",1972}, {"Scott",1976},

{"Wilkes",1967}, {"Hamming",1968}

};

m["Ritchie"] = 1983;

for(auto x: m) cout << '{' << x.first << ',' << x.second << '}';

unordered_map<string,int> um {

{"Dijkstra",1972}, {"Scott",1976},

{"Wilkes",1967}, {"Hamming",1968}

};

um["Ritchie"] = 1983;

for(auto x: um) cout << '{' << x.first << ',' << x.second << '}';

 

Итератор объекта m будет перебирать элементы в алфавитном порядке, а итератор объекта um – нет (если только не по чистой случайности). Процессы поиска элементов в m и um абсолютно различны. Поиск в m осуществляется за log2(m.size()) сравнений, в то время как поиск в um приводит к одному вызову хеш-функции и одной или более проверке равенства. Для нескольких элементов (скажем, для нескольких десятков), сложно сказать, какой из этих вариантов будет работать быстрее. Для большого количества элементов (например, если речь идет о тысячах) - поиск в unordered_map может быть значительно быстрее чем в map.

Продолжение следует.

См. также:

  • Standard: 23.5 Unordered associative containers.

 

Std::tuple

Определен в <tuple>. Представляет собой упорядоченную последовательность из N значений, где N является константой от 0 до большого значения, зависящего от реализации. Вы можете рассматривать кортеж (tuple) как неименованную структуру, члены которой соответствуют типам элементов кортежа. Следует отметить, что элементы в tuple хранятся компактным образом; кортеж не является связным списком.

Типы элементов кортежа могут задаваться явно, а могут выводиться (при вызове make_tuple()), доступ к элементам осуществляется с помощью индекса (начинается с 0) путем вызова метода get():

tuple<string,int> t2("Kylling",123);

// t будет типа tuple<string,int,double>

auto t = make_tuple(string("Herring"),10, 1.23);

string s = get<0>(t);

int x = get<1>(t);

double d = get<2>(t);

 

Кортежи используются (явно или неявно) для хранения гетерогенного списка элементов, известного во время компиляции, и когда нет желания определять для этого именованный класс. Например, tuple используетсядля хранения элементоввнутри std::function и std::bind.

Наиболее часто используется кортеж из двух элементов, т.е. пара (pair). Однако пара напрямую поддерживается классом std::pair из стандартной библиотеки(20.3.3 Pairs). pair может использоваться для инициализации кортежа, но обратное преобразование невозможно.

Операторы сравнения (==,!=, <, <=, > и >=) определены для кортежей совместимых типов.

См. также:

  • Standard: 20.5.2 Class template tuple
  • Variadic template paper
  • Boost::tuple

 

Метапрограммирование и характеристики типов

Простите. Возвращайтесь к этому разделу позже.

 

Std::function и std::bind

Стандартные функторы bind и function определены в <functional> (вместе со многими другими полезными функторами); они предназначены для работы с функциями и их аргументами. bind принимает функцию (или функтор, или все что угодно, что можно вызвать с помощью (…)) и возвращает функтор с одним или более «привязанным» (bound) аргументом, либо с другим порядком аргументов. Например:

int f(int,char,double);

// выводим тип возвращаемого значения

auto ff = bind(f,_1,'c',1.2);

// f(7,'c',1.2);

int x = ff(7);

 

Такая привязка аргументов обычно называется «каррированием» (currying). _1 – это заместитель (placeholder), указывающий на первый аргумент функции ff, который будет передан функции f при вызове ее через ff. Первый аргумент называется _1, второй - _2 и т.д. Например:

int f(int,char,double);

// изменяем порядок аргументов на противоположный

auto frev = bind(f,_3,_2,_1);

// f(7,'c',1.2);

int x = frev(1.2,'c',7);

 

Обратите внимание, как auto избавляет от необходимости явно указывать тип возвращаемого значения метода bind.

Связать аргументы перегруженной функции нельзя, для этого мы должны явно указать, какая нам нужна версия перегруженной функции:

int g(int);

// g() перегружена

double g(double);

// ошибка: какая версия g()?

auto g1 = bind(g,_1);

// ok (но выглядит ужасно)

auto g2 = bind((double(*)(double))g,_1);

 

Существует две версии метода bind(): одна версия показана выше, вторая «устаревшая» - требует явного указания типа возвращаемого значения:

// явно указываем тип возвращаемого значения

auto f2 = bind<int>(f,7,'c',_1);

// f(7,'c',1.2);

int x = f2(1.2);

 

Необходимость (и широкая популярность) второй версии была обусловлена тем, что первую (более удобную в использовании) версию нельзя было реализовать в С++98.

function – это тип, который может хранить практически что угодно, что можно вызвать с помощью синтаксиса вызова метода (...). В частности, результат вызова метода bind можно присвоить типу function. function использовать очень просто. Например:

// создаем функтор

function<float (int x, int y)> f;

// получаем нечто, что можно вызвать с помощью ()

struct int_div {

float operator()(int x, int y) const {

return ((float)x)/y;

};

};

// присваиваем

f = int_div();

// вызываем функор

cout << f(5, 3) << endl;

// спокойно передаем куда-либо

std::accumulate(b,e,1,f);

 

Функцию-член можно рассматривать как свободную функцию с одним аргументом. Мы можем рассматривать function, как замену функторов стандартной библиотеки С++98: mem_fun_t, pointer_to_unary_function и т.д. Аналогичным образом, мы можем рассматривать bind(), как замену bind1() и bind2().

См. также:

  • Standard: 20.7.12 Function template bind, 20.7.16.2 Class template function
  • Herb Sutter: Generalized Function Pointers. August 2003.
  • Douglas Gregor: Boost.Function.
  • Boost::bind

 

Unique_ptr

· unique_ptr (определен в <memory>) и обеспечивает семантику строгого владения.

o Владеет объектом, на который хранит указатель.

o Не CopyConstructable и не CopyAssignable, однако MoveConstructible и MoveAssignable.

o При собственном удалении (например, при выходе из области видимости (6.7)) уничтожает объект (на который хранит указатель) с помощью заданного метода удаления (с помощью deleter-а).

· Использование unique_ptr дает:

o безопасность исключений при работе с динамически выделенной памятью,

o передачу владения динамически выделенной памяти в функцию,

o возвращения динамически выделенной памяти из функции,

o хранение указателей в контейнерах

· «Это то, чем должен был быть auto_ptr» (но который мы не могли реализовать на С++98)

unique_ptr реализован главным образом на основе rvalue-ссылок и семантике перемещения.

Вот типовой фрагмент небезопасного с точки зрения исключений кода:

X* f()

{

X* p = new X;

// делаем что-то еще, возможна генерация исключения

return p;

}

 

Решение заключается в том, чтобы unique_ptr хранил указатель на память в куче:

X* f()

{

// или {new X}, но не = new X

unique_ptr<X> p(new X);

// делаем что-то еще, возможна генерация исключения

return p.release();

}

 

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

unique_ptr<X> f()

{

// или {new X}, но не = new X

unique_ptr<X> p(new X);

// делаем что-то еще, возможна генерация исключения

return p; // владение передается из f()

}

 

Мы можем использовать f следующим образом:

Void g()

{

// перемещаем с помощью конструктора перемещения

unique_ptr<X> q = f();

// используем q

q->memfct(2);

// копируем объект, на который указываем

X x = *q;

//...

} // q и объект, которым он владеет удаляется

 

unique_ptr обладает «семантикой перемещения», так q инициализируется результатом метода f(), путем передачи владения.

Используйте unique_ptr в качестве элементов контейнеров вместо стандартных указателей для обеспечения безопасности исключений и гарантированное освобождение элементов контейнера.

vector<unique_ptr<string>> vs {

new string{"Doug"}, new string{"Adams"} };

 

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

См. также:

  • the C++ draft section 20.7.10
  • Howard E. Hinnant: unique_ptr Emulation for C++03 Compilers.

 

Shared_ptr

shared_ptr используется для совместного владения; т.е. когда два участка кода должны иметь доступ к некоторым данным, но ни один из них не обладает эксклюзивным владением ими (в плане ответственности за удаление объекта). shared_ptr – это указатель со счетчиком ссылок, который удаляет хранимый объект, когда счетчик уменьшается до нуля. Ниже представлен искусственный пример:

Void test()

{

shared_ptr<int> p1(new int); // счетчик равен 1

{

shared_ptr<int> p2(p1); // счетчик равен 2

{

shared_ptr<int> p3(p1); // счетчик равен 3

} // счетчик уменьшается до 2

} // счетчик уменьшается до 1

} // здесь счетчик уменьшается до 0 и int удаляется.

 

Более реалистичным примером может служить указатель на узлы в некотором графе объектов, при этом некоторый код хочет удалить указатель на узел, но не знает, держит ли кто-то еще указатель на этот узел или нет. А что если узел содержит ресурсы, требующие некоторого действия в деструкторе (например, это можем быть дескриптор файла, который должен быть закрыт при удалении узла). Можно считать shared_ptr неким аналогом сборщику мусора, в тех случаях, когда у вас просто не достаточно мусора, чтобы использование сборщика мусора стало экономически выгодной, ваша среда выполнения его не поддерживает, или же память – является не единственным ресурсом, которым нужно управлять (например, когда речь идет о файловых дескрипторах).. Например:

// обратите внимание: можем быть множество

// указателей на Node из разных мест.

struct Node {

shared_ptr<Node> left;

shared_ptr<Node> right;

File_handle f;

//...

};

 

В данном случае, деструктор Node (неявно сгенерированный деструктор нас вполне устроит) удаляет подузлы; т.е. будут вызваны деструкторы для left и right. Поскольку поле left – это shared_ptr, то Node (на который указывает left), будет удален только если left – последний, кто хранит на него ссылку; поведение поля right аналогично, при этом деструктор поля f делает все необходимое для освобождения собственных ресурсов.

Обратите внимание, что не нужно использовать shared_ptr просто для передачи владения из одного места в другое; для этого предназначен unique_ptr и делает о

Поделиться:





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



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