5232 / 3204 / 362
Регистрация: 12.12.2009
Сообщений: 8,143
Записей в блоге: 2
1

С++ идиомы

31.07.2016, 19:20. Показов 116934. Ответов 34
Метки нет (Все метки)

Author24 — интернет-сервис помощи студентам
Перевод статей 1 и 2. Будет постепенно обновляться. Желающие внести вклад могут писать в ЛС.

 Комментарий модератора 
Тема открыта, просьба добавлять только посты с переводом, обсуждение здесь


Переведенные идиомы:
self-assignment in an assignment operator
Scope Guard
Shrink-to-fit
Checked delete
Pointer To Implementation
Получение адреса(ака взятие адреса aka Address-of)
nullptr
Iterator Pair
Coercion by Member Template
14
Programming
Эксперт
94731 / 64177 / 26122
Регистрация: 12.04.2006
Сообщений: 116,782
31.07.2016, 19:20
Ответы с готовыми решениями:

С++ идиомы - обсуждение
Тема создана для вопросов и обсуждений С++ идиом

Как и какие идиомы и паттерны можно (и лучше) применять?
Здравствуйте. Писал я проги для себя ну и так по мелочи, для абы кого, да и проги абы как, не...

Идиомы программирования
диома программирования — это некоторое часто применяемое действие в прораммировании. Это самый...

Английские идиомы: как правильно перевести in its own right?
Как правильно перевести in its own right? Например, вот в таком контексте: Добавлено через 3...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
34
829 / 253 / 34
Регистрация: 27.07.2016
Сообщений: 497
Записей в блоге: 1
01.08.2016, 11:33 2
Scope Guard

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

Мотивация:
Идиома RAII позволяет захватывать ресурсы в конструкторе и освобождать их в деструкторе, при достижении конца области видимости или из-за исключения. В RAII ресурс освобождает всегда. Это может быть не очень гибким решением. Может потребоваться освободить ресурсы в случае возникновения исключения и не освобождать при нормальном завершении.

Решение и пример кода:
Типичная реализация идиомы RAII с проверкой необходимости освобождения ресурса.

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ScopeGuard
{
public:
  ScopeGuard () 
   : engaged_ (true) 
  { /* Захват ресурса */ }
  
  ~ScopeGuard ()  
  { 
    if (engaged_) 
     { /* Освобождение ресурса */} 
  }
  void release () 
  { 
     engaged_ = false; 
     /* Ресурс не будет освобожден в деструкторе */ 
  }
private:
  bool engaged_;
};
void some_init_function ()
{
  ScopeGuard guard;
  // В случае возникновения исключения ресурс будет освободжен
  // прим. переводчика: в случае каких-то других ошибок мы можем просто выйти из функции, ресурс также будет освобожден
  guard.release (); // При нормальном завершении ресурс освобождать не нужно
}
9
5232 / 3204 / 362
Регистрация: 12.12.2009
Сообщений: 8,143
Записей в блоге: 2
01.08.2016, 11:54  [ТС] 3
self-assignment in an assignment operator (самоприсваивание в операторе присваивания)

T::operator= который обрабатывает случай, где левый и правый операнды являются одним и тем же объектом.
C++
1
2
3
4
5
6
7
8
9
T& operator= (const T& that)
{
    if (this == &that)
        return *this;
 
    // handle assignment here
 
    return *this;
}
Замечания:

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

Существуют и другие техники, которые в некоторых случаях могут быть более удобными, но они не применимы во всех ситуациях. Например если все члены класса T (скажем mem1, mem2, ..., memN) предоставляют функцию swap(), то можно использовать следующий код
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
T& operator= (T that)
{
    // that создан конструктором копий
 
    mem1.swap (that.mem1);
    mem2.swap (that.mem2);
 
    ...
 
    memN.swap (that.memN);
 
    // теперь то, что изначально было  this->mem1, this->mem2, etc. разрушается
    // при разрушении объекта that 
    // новые данные лежат в *this 
 
    return *this;
}
6
829 / 253 / 34
Регистрация: 27.07.2016
Сообщений: 497
Записей в блоге: 1
01.08.2016, 20:51 4
Shrink-to-fit (уменьшить до размеров)

Задачи:
Уменьшить ёмкость (capacity) контейнера, до размера, достаточного для содержания элементов.

Также известен как
"Swap-To-Fit", введенный Скотом Майерсом в книге "Effective STL" ("Эффективное использование STL").

Мотивация:
Контейнеры в стандартной библиотеке часто выделяют памяти под большее число элементов, чем находится в контейнере.
Это позволяет оптимизировать расширение контейнера за счет более редких выделений памяти.
Но, когда контейнер уменьшается (или когда запас израсходаван не полностью, прим. переводчика), память так и остается занятой, хотя, фактически, она не используется. Это не нужный перерасход памяти. Данная (shrink-to-fit) идиома была разработана чтобы уменьшить расход до минимального требуемого контейнеру количества памяти, тем самым экономя ресурсы.

Решение и пример кода:
Данная идиома очень проста, как показано ниже.
C++
1
2
3
std::vector<int> v;
//v будет обменен с временной копией, которая оптимально использует запас памяти
std::vector<int>(v).swap(v);
Первая половина этой инструкции - std::vector<int>(v), создает временный вектор целых чисел, гарантируя, что выделенной памяти достаточно для хранения всех элементов вектора v, который передан в параметре. Также эти элементы копируются во временный вектор.
Вторая половина инструкции - .swap(v) обменивает элементы вектора v и временного вектора, используя не выбрасывающую исключений функцию-член swap, что является очень эффективным средством, которое сводится к обмену внутренних указателей между векторами или чуть более того.
После этого временный вектор удаляется, очищая память и удаляя элементы, которые первоначально находились в векторе v.
Вектор v же имеет ровно столько памяти, сколько нужно для хранения элементов.

ISO/IEC 14882:1998 не гарантирует такое поведение для конструктора копирования. Как гарантировать такое поведение?

Более надежным решением (в частности std::string и std::vector могут быть реализованы с использованием подсчета ссылок и тогда конструктор копирования может "скопировать" всю избыточную память) будет использование конструктора диапазонов, вместо конструктора копирования:
C++
1
2
3
std::vector<int> v;
//v будет обменен с временной копией, которая оптимально использует запас памяти
std::vector<int>(v.begin(), v.end()).swap(v);
Решение в C++11:
В C++11 некоторые контейнеры предоставляют функцию-член shrink_to_fit, например vector, deque, basic_string. shrink_to_fit запрашивает уменьшение capacity до size, Но данный запрос не является обязательным к исполнению.
8
829 / 253 / 34
Регистрация: 27.07.2016
Сообщений: 497
Записей в блоге: 1
01.08.2016, 20:51 5
Checked delete

Цель:
Повышение безопасности при использовании delete expression.

Мотивация и пример проблемного кода:
Стандарт C++ (пункт 5.3.5/5) позволяет использовать в delete-expression указатель на не полный тип, но при этом, если деструктор или функция освобождения памяти объекта не являются тривиальными, то это приведет к неопределенному поведению.
Цитата Сообщение от 5.3.5/5
If the object being deleted has incomplete class type at the point of deletion and the complete class has a non-trivial destructor or a deallocation function, the behavior is undefined.
Некоторые компиляторы в таком случае выдают предупреждение, но могут этого и не сделать, либо программист может игнорировать или отключить предупреждения.

В следующем примере в main.cpp определяется объект типа Object. В функции main() вызывается функция delete_object(), определенная в deleter.cpp, где нет определения класса Object, а есть лишь его объявление.
Вызов delete в данной функции приводит к неопределенному поведению.
C++
1
2
3
4
5
6
////////////////////
// File: deleter.hpp
////////////////////
// Объявляем Object, но не определяем его
struct Object;
void delete_object(Object* p);
C++
1
2
3
4
5
6
7
////////////////////
// File: deleter.cpp
////////////////////
#include "deleter.hpp"
 
// Удаляем объект типа Object, не имея его определения
void delete_object(Object* p) { delete p; }
C++
1
2
3
4
5
6
7
8
9
10
11
////////////////////
// File: object.hpp
////////////////////
struct Object
{
  //Этот деструктор не будет вызван при удалении, 
  //если не будет определения типа при вызове delete
  ~Object() {
     // ...
  }
};
C++
1
2
3
4
5
6
7
8
9
10
////////////////////
// File: main.cpp
////////////////////
#include "deleter.hpp"
#include "object.hpp"
 
int main() {
  Object* p = new Object;
  delete_object(p);
}
Решение и пример кода:
Идиома checked delete полагается на вызов шаблонной функции для удаления объекта, а не на прямой вызов delete, который может привести к неопределенному поведению для объявленных, но неопределенных типов.
Ниже приводится реализация шаблонна функции boost::checked_delete из Boost Utility library.
Её использование вызывает ошибку компиляции при использовании sizeof для параметра шаблона T, если T - не полный тип.
Если T объявлен, но не определен, то sizeof(T) будет генерировать ошибку компиляции или возвращать нулевое значение, в зависимости от компилятора.
Если sizeof(T) вернет ноль, то произойдет ошибка компиляции, т.к. объявляется массив с отрицательным количеством элементов (-1). Имя type_must_be_complete в данном случае появляется в сообщении об ошибке и позволяет понять что произошло.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<class T> 
inline void checked_delete(T * x)
{
    typedef char type_must_be_complete[ sizeof(T)? 1: -1 ];
    (void) sizeof(type_must_be_complete);
    delete x;
}
template<class T> 
struct checked_deleter : std::unary_function <T *, void>
{
    void operator()(T * x) const
    {
        boost::checked_delete(x);
    }
};
Примечание: этот метод также применим к delete[].

Предупреждение: std::auto_ptr не использует никакого эквивалента checked delete. Поэтому инстанцирование std::auto_ptr с неполным типом может привести к неопределенному поведению в деструкторе, если в момент объявления std::auto_ptr тип параметра шаблона определен не полностью.
12
5232 / 3204 / 362
Регистрация: 12.12.2009
Сообщений: 8,143
Записей в блоге: 2
02.08.2016, 08:12  [ТС] 6
Pointer To Implementation (pImpl)

Идиома "pointer to implementation" (pImpl) также называется "opaque pointer" (дословный перевод "непрозрачный указатель"), это способ предоставления данных и в еще один уровень абстракции в реализации классов.

В С++ вы должны написать декларацию переменных-членов класса внутри определения класса, эти члены должны быть публичны (прим. переводчика - думаю речь идет об интерфейсе, иначе зачем членам быть публичными) и поскольку для членов выделяется память абстракция реализации не возможна для "всех" классов.
Тем не менее, за счет дополнительного указателя и вызова функции, вы можете иметь такой уровень абстракции через указатель на реализацию.

Допустим вы написали такой класс:
C++
1
2
3
4
5
6
7
class Book
{
public:
  void print();
private:
  std::string  m_Contents;
}
Кто-то, кто работает с классом Book должен знать лишь о методе print(), но что произойдет если вы хотите добавить больше деталей в ваш класс:
C++
1
2
3
4
5
6
7
8
class Book
{
public:
  void print();
private:
  std::string  m_Contents;
  std::string  m_Title;
}
В этом случае все, кто используют ваш класс, должны перекомпилировать свой код, несмотря на то, что они по прежнему используют только вызов метода print().

pImpl может реализовывать следующий паттерн таким образом, что описанная выше ситуация не является проблемой.
C++
1
2
3
4
5
6
7
8
9
10
11
/* public.h */
class Book
{
public:
  Book();
  ~Book();
  void print();
private:
  class BookImpl;
  BookImpl* m_p;
}
а в отдельном "внутреннем" хедере
C++
1
2
3
4
5
6
7
8
9
10
11
/* private.h */
#include "public.h"
#include <iostream>
class Book::BookImpl
{
public:
  void print();
private:
  std::string  m_Contents;
  std::string  m_Title;
}
Реализация методов класса Book может выглядеть так

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Book::Book()
{
  m_p = new BookImpl();
}
 
Book::~Book()
{
  delete m_p;
}
 
void Book::print()
{
  m_p->print();
}
 
/* функции BookImpl */
 
void Book::BookImpl::print()
{
  std::cout << "print from BookImpl" << std::endl;
}
Используем Book в функции main:
C++
1
2
3
4
5
6
int main()
{
  Book *b = new Book();
  b->print();
  delete b;
}
Вы также можете использовать std::unique_ptr<BookImpl> или эквивалент для управления внутренним указателем.
8
☆ Форумчанин(FSC)☆
911 / 292 / 27
Регистрация: 28.04.2013
Сообщений: 2,466
Записей в блоге: 10
09.08.2016, 19:40 7
Получение адреса(ака взятие адреса aka Address-of)

Назначение:
Поиск адреса обьекта класса который имеет перегруженный унарный оператор "амперсанд"(&).

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

В примере ниже не удается скомпилировать функцию main потому что оператор & класса nonaddressable является закрытым членом этого класса.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
class nonaddressable 
{
public:
    typedef double useless_type;
private:
    useless_type operator&() const;
};
 
int main()
{
  nonaddressable na;
  nonaddressable * naptr = &na; // Здесь ошибка компиляции
}
Решение и пример использования
Идиома получения адреса запрашивает адрес обьекта используя серию преобразований

C++
1
2
3
4
5
6
7
8
9
10
template <class T>
T * addressof(T & v)
{
  return reinterpret_cast<T *>(& const_cast<char&>(reinterpret_cast<const volatile char &>(v)));
}
int main()
{
  nonaddressable na;
  nonaddressable * naptr = addressof(na); // ошибки больше нет
}
Где используется?
Используется в библиотеке boost для получения адреса
Эта функция уже пристутствует в заголовке <memory> нового стандарта C++(C++ 11)
2
161 / 153 / 92
Регистрация: 18.11.2015
Сообщений: 677
16.08.2016, 15:46 8
Nullptr

Задачи:
Научиться отличать число 0 от нулевого указателя


Мотивация:

На протяжении многих лет С++ имело позорный недостаток, у С++ не было ключевого слова, которое бы обозначало нулевой указатель. С++ 11 избавился от этого недостатка. Строгая типизация С++ делает определение глобальной константы NULL в стиле языка C почти что бесполезной в выражениях, например:
C++
1
2
3
4
5
6
7
8
9
10
// Если бы мы попробовали определить NULL в C++, вот так
#define NULL ((void *)0)
 
// То тогда...
 
char * str = NULL; // Ошибка: Нельзя автоматически привести тип void * к char *
 
void (C::*pmf) () = &C::func;
 
if (pmf == NULL) {} // Ошибка: Нельзя автоматически привести тип void * к типу указателя на функцию-член
В С++
C++
1
#define NULL 0
и
C++
1
#define NULL 0L
являются правильными (но не совсем) определениями константы нулевого указателя.

Проблема самого первого примера в том, что С++ запрещает приведение из типа void *, даже когда его значение является константным нулем. Но, для простого константного нуля С++ имеет преобразование int в указатель (еще short в указатель, long в указатель и т.п.).Это имеет свои минусы. Приведем пример на работе с перегруженными функциями:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
 
#define NULL 0
 
void func(int var) { std::cout << "Int" << std::endl; };
void func(double * ptr) { std::cout << "Double *" << std::endl; };
 
int main()
{
    func(static_cast <double *>(NULL)); // вызывается func(double *), как мы и хотели
 
    func(NULL); // запись интерпретируется, как func(0) - вызывается func(int).
                // но программист, скорее всего, хотел вызвать func(double *),
                // потому что NULL является константой нулевого УКАЗАТЕЛЯ,
                // которую мы определили ранее.
 
    return 0;
}
После обработки кода препроцессором, код в функции main превратиться в:
C++
1
2
3
4
5
func(static_cast <double *>(0));
 
func(0);
 
return 0;
После запуска этого кода, на экране будет:
Код
Double *
Int
Таким образом, чтобы использовать #define NULL 0 так, как задумывалось (с указателями), нужно всегда писать что типа static_cast<double *>(NULL)


Использование #define NULL 0 имеет свою кучку проблем. Помимо проблемы с перегруженными функциями, C++ требует того, чтобы NULL было определено целочисленным константным выражением со значением 0. Поэтому, в отличии от С, нулевой указатель не может быть определен как ((void *)0) в стандартной библиотеке C++. Более того, конкретная форма определения оставлена для той или иной реализации, что значит, что 0 и 0L - подходящие определения, наряду с некоторыми другими.



Решение и пример использования
Идиома нулевого указателя решает некоторые вышеперечисленные проблемы и может быть использована снова и снова. Будущий пример является очень близким по функционалу решением, относительно ключевого слова nullptr, добавленного в С++ 11, и использует только стандартные приемы, которые были доступны еще до С++ 11.

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const // Объект константный...
class nullptr_t 
{
  public:
    template<class T>
    inline operator T*() const // Может быть приведен к любому типу нулевого указателя (не на член класса)
    { return 0; }
 
    template<class C, class T>
    inline operator T C::*() const   // или любому типу нулевого указателя на член
    { return 0; }
 
  private:
    void operator&() const;  // мы не можем взять адрес nullptr
 
} nullptr = {};
Код ниже показывает, как можно использовать класс, определенный выше (предполагается, что пользователь уже сделал #include класса выше)

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <typeinfo>
//#include "nullptr_t"
struct C
{
    void func();
};
 
template<typename T>
void g(T* t) {}
 
template<typename T>
void h(T t) {}
 
void func(double *) {}
void func(int) {}
 
int main(void)
{
    char * ch = nullptr;        // Ок
    func(nullptr);             // Вызывает func(double *)
    func(0);                   // Вызывает func(int)
    void (C::*pmf2)() = 0;      // Ок
    void (C::*pmf)() = nullptr; // Ок
    nullptr_t n1, n2;
    n1 = n2;
    nullptr_t *null = &n1;    // Адрес мы взять не можем
    if (nullptr == ch) {}       // Ок
    if (nullptr == pmf) {}      // Это тоже работает, но не на g++ 4.1.1-4.5 из-за бага #33990
                                // для GCC 4: if ((typeof(pmf))nullptr == pmf) {}
    const int n = 0;
    if (nullptr == n) {}        // Не должно компилироваться, но только Comeau показывает ошибку
 
    int p = 0;
    if (nullptr == p) {}      // Не работает
    g (nullptr);              // Не возможно вывести тип T
 
    int expr = 0;
    char* ch3 = expr ? nullptr : nullptr; // ch3 - нулевой указатель
 
    char* ch4 = expr ? 0 : nullptr;     // ошибка, типы не совместимы
    int n3 = expr ? nullptr : nullptr;  // ошибка, нельзя преобразовать nullptr в int
    int n4 = expr ? 0 : nullptr;        // ошибка, типы не совместимы
 
    h(0);                // Выводит T = int
    h(nullptr);          // Выводит T = nullptr_t
    h((float*) nullptr); // Выводит T = float*
 
    sizeof(nullptr);     // Ок
    typeid(nullptr);     // Ок
    throw nullptr;       // Ок
}
К сожалению, похоже, есть баг в компиляторе gcc 4.1.1, который не распознает сравнение nullptr с указателем на функцию-член (pmf). Код выше успешно компилируется, если убрать строки с ошибками, которые мы допустили в демонстрационных целях (26, 31, 34, 35, 40, 41, 42, 50)

Заметьте, что идиома нулевого указателя использует идиому Return Type Resolver, чтобы автоматически вывести нулевой указатель правильного типа, в зависимости от типа объекта, к которому мы его присваиваем. Например, если nullptr присваивают к char *, создается функция преобразования с char параметром шаблона.


Последствия
Есть некоторые недостатки этого приема, в список этих недостатков входят следующие пункты:
  • Чтобы использовать nullptr, надо постоянно писать #include "nullptr_t". В С++ 11 и выше nullptr является ключевым словом и не требует включения заголовочного файла (но для использования std::nullptr_t заголовочный файл включать все равно надо (<cstddef>))
  • Компиляторы, по историческим причинам, произвели неудовлетворительные диагностики, когда был использован вышепоказанный код
5
Мозгоправ
1745 / 1039 / 468
Регистрация: 01.10.2018
Сообщений: 2,138
Записей в блоге: 2
03.11.2019, 18:47 9
Iterator Pair

Итераторная пара

Задача

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

Также известно как

Иногда это называют итераторным диапазоном (Iterator Range).

Проблема

Хорошо известно, что для создания вектора vector<int> из другого vector<int> используется конструктор копии. Также для создания вектора vector<int> из другого vector<int> можно использовать идиому Приведение типа посредством шаблонного метода (Coercion by Member Template), применённую к конструктору шаблонного члена. Пример кода показан ниже.
C++
1
2
3
4
5
6
7
8
template <class T>
class vector
{
  public:
    vector (const vector<T> &); // конструктор копии
    template <class U>
    vector (const vector<U> &); // конструктор, использующий Coercion by Member Template.
};
Однако интерфейс вектора все еще недостаточно гибок для некоторых применений. Например, вектор не может создавать себя из списка, из множества или из массива POD.
C++
1
2
3
4
5
6
7
8
9
10
11
template <class T>
class vector
{
  public:
    vector (const list<T> &); 
    // конструктор должен знать интерфейс list<T> (не обязательно std::list)
    vector (const set<T> &); 
    // конструктор должен знать интерфейс set<T> (не обязательно std::set)
    vector (const T * pod_array);
    // а этот конструктор не знает где заканчивается массив pod_array – слишком негибко!
};
Итераторная пара – это идиома, которая решает эту проблему. Она основана на шаблоне проектирования итератора (внезапно!), задачей которого является предоставление объекта, который проходит по некоторой сложной структуре, абстрагируясь от предположений о реализации этой структуры.

Решение и пример кода

Пара итераторов используется для обозначения начала и конца диапазона значений. Благодаря шаблону проектирования итераторов, кто бы ни использовал эту идиому (в нашем примере для вектора), может получить доступ к диапазону, абстрагируясь от деталей реализации структуры данных. Единственное требование заключается в том, что итераторы должны предоставлять минимальный фиксированный интерфейс. Например, предоставлять оператор префиксного инкремента.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
template <class T>
class vector
{
    T * mem;
  public:
    template <class InputIterator>
    vector (InputIterator begin, InputIterator end) // конструктор, использующий итераторную пару
    {
      // запрос достаточного количества памяти и сохранение в mem
      mem=new T[std::distance(begin, end)];
      for (int i = 0; begin != end; ++i)
      {
        mem[i] = *begin;
        ++begin;
      }
    }
};
 
int main (void)
{
  std::list<int> l(4);
  std::fill(l.begin(),l.end(), 10);    // заполнение списка с использованием пары итераторов
  std::set<int> s(4);
  std::fill(s.begin(),s.end(), 20);    // заполнение множества с использованием пары итераторов
 
  std::vector<int> v1(l.begin(), l.end());  // создание вектора с использованием пары итераторов
  std::vector<int> v2(s.begin(), s.end());  // создание ещё одного вектора
}
Идиома итераторной пары часто сочетается с шаблонными методами, потому что точный тип итераторов заранее не известен. Это может быть set<T>::iterator или list<T>::iterator или массив POD. Независимо от типа, любой общий алгоритм, написанный в терминах итераторных пар, работает. Часто полезно показать, что модель должна реализовывать итераторные типы. В приведенном выше примере итераторы требуют от модели наличия как минимум InputIterator. Более подробная информация о категориях (тегах) итераторов и их использовании описана в идиоме Диспетчеризация тегов.

Иногда без использования идиомы итераторной пары обойтись невозможно. Например, при создании std::string из буфера символов, в котором присутствуют нулевые символы, применение идиомы итераторной пары просто необходимо.
C++
1
2
3
4
5
6
7
char buf[] = { 'A', 'B', 0, 'C', 0, 'D'};
std::string str1 (buf); // only creates "AB"
std::string str2 (buf, buf + sizeof (buf) / sizeof (*buf)); // Использование итераторной пары. Создаёт "AB_C_D"
// buf – это начало диапазона, а buf + sizeof (buf) / sizeof (*buf) – конец диапазона.
 
std::cout << str1 << " length = " << str1.length() << std::endl; // AB длина = 2
std::cout << str2 << " length = " << str2.length() << std::endl; // AB_C_D длина = 6
Известные применения

Все стандартные контейнеры.

Связанные идиомы
1
Мозгоправ
1745 / 1039 / 468
Регистрация: 01.10.2018
Сообщений: 2,138
Записей в блоге: 2
05.11.2019, 14:53 10
Coercion by Member Template

Приведение типа посредством шаблонного метода

Задача

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

Проблема

Часто бывает полезно расширить отношения между двумя типами до специализации шаблонов классов с этими типами. Например, предположим, что класс D наследуется от класса B. Указатель на объект класса D может быть присвоен указателю на класс B. C++ поддерживает это неявным образом. Однако типы, составленные из этих типов, не разделяют отношения родительских типов. Это относится и к шаблонам классов, поэтому объект Helper<D> обычно нельзя присвоить объекту Helper<B>.
C++
1
2
3
4
5
6
7
8
9
10
11
12
class B {};
class D : public B {};
template <class T>
class Helper {};
 
B *bptr;
D *dptr;
bptr = dptr; // OK; разрешено в C++
 
Helper<B> hb;
Helper<D> hd; 
hb = hd; // Не разрешено, но может быть очень полезно
Бывают случаи, когда такие преобразования полезны, например, для преобразования из std::unique_ptr<D> в std::unique_ptr<B>. Это интуитивно понятно, но не поддерживается без использования идиомы Приведение типа посредством шаблонного метода.

Решение и пример кода

Определите в шаблонном классе шаблонные методы, которые полагаются на неявные преобразования типов, которые поддерживаются типами параметров. В следующем примере шаблонный конструктор и оператор присваивания работают для любого типа U, для которого разрешена инициализация или присвоение T* из U*.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
template <class T>
class Ptr
{
  public:
    Ptr () {}
 
    Ptr (Ptr const & p)
      : ptr (p.ptr)
    {
      std::cout << "Copy constructor\n";
    }
 
    // Поддержка приведения типа с использованием шаблонного конструктора.
    // Это не конструктор копирования, но работает аналогично.
    template <class U>
    Ptr (Ptr <U> const & p)
      : ptr (p.ptr) // Требуется неявное преобразование типа из U в T
    {
      std::cout << "Coercing member template constructor\n";
    }
 
    // Копирующий оператор присваивания.
    Ptr & operator = (Ptr const & p)
    {
      ptr = p.ptr;
      std::cout << "Copy assignment operator\n";
      return *this;
    }
 
    // Поддержка приведения типа с использованием шаблонного оператора присваивания.
    // Это не копирующий оператор присваивания, но работает аналогично.
    template <class U>
    Ptr & operator = (Ptr <U> const & p)
    {
      ptr = p.ptr; // Требуется неявное преобразование типа из U в T
      std::cout << "Coercing member template assignment operator\n";
      return *this;
    } 
 
    T *ptr;
};
 
int main (void)
{
   Ptr <D> d_ptr;
   Ptr <B> b_ptr (d_ptr); // Теперь поддерживается
   b_ptr = d_ptr;         // Теперь поддерживается
}
Другое использование этой идиомы – разрешить присваивание массива указателей на класс массиву указателей на базовый класс. Учитывая, что класс D унаследован от класса B, D-объект является B-объектом. Однако массив объектов D не является массивом объектов B. Это запрещено в C++ из-за возможного неправильного вычисления размера массива на основании размера элемента. (Объект производного класса, скорее всего, будет занимать больше памяти, чем объект базового класса. Поэтому если предположить, что массив D-объектов, это массив B-объектов, то часть массива будет утеряна при расчёте объёма занимаемой памяти, исходя из размера B-объекта, а остальные объекты, за исключением первого, будут некорректны из-за неправильного позиционирования на начало объекта. – прим. перев.) Ослабление этого правила для множества указателей может быть полезным. Например, массив указателей на D должно быть возможно присвоить массиву указателей на B (при условии, что деструктор B является виртуальным). Этого можно достичь с помощью рассматриваемой идиомы, но требуется дополнительная осторожность, чтобы предотвратить копирование массива указателей на базовый тип в массив указателей на производный тип. Для этого можно использовать специализации шаблонных методов или SFINAE.

В следующем примере используется шаблонный конструктор и шаблонный оператор присваивания. Из-за объявления Array<U *> они будут разрешать копирование массивов указателей только тогда, когда типы элементов различаются.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
template <class T>
class Array
{
  public:
    Array () {}
    Array (Array const & a)
    {
      std::copy (a.array_, a.array_ + SIZE, array_);
    }
 
    template <class U>
    Array (Array <U *> const & a)
    {
      std::copy (a.array_, a.array_ + SIZE, array_);
    }
 
    template <class U>
    Array & operator = (Array <U *> const & a)
    {
      std::copy (a.array_, a.array_ + SIZE, array_);
    }
 
    enum { SIZE = 10 };
    T array_[SIZE];
};
Многие умные указатели, такие как std::unique_ptr и std::shared_ptr, используют эту идиому.

Предостережения

Типичной ошибкой в реализации идиомы Приведения типа посредством шаблонного метода является отсутствие нешаблонного конструктора копирования или нешаблонного копирующего оператора присваивания при наличии шаблонного конструктора копирования и шаблонного копирующего оператора присваивания. Компилятор автоматически создаёт конструктор копирования и копирующий оператор присваивания, если класс не объявляет их, что может вызвать скрытые и неочевидные ошибки при использовании этой идиомы.

Известные применения
  • std::unique_ptr
  • std::shared_ptr

Связанные идиомы
1
Эксперт С++
8482 / 6149 / 615
Регистрация: 10.12.2010
Сообщений: 28,683
Записей в блоге: 30
12.02.2020, 15:45 11
CoreHard Conf 2016: Идиомы C++
2
329 / 149 / 33
Регистрация: 29.06.2019
Сообщений: 1,429
12.02.2021, 16:38 12
Цитата Сообщение от HelicopterK52 Посмотреть сообщение
RAII... Гарантировать освобождение ресурсов при возникновении исключения, но не освобождать ресурсы при нормальном завершении.
доп. на habr'e
Почему-то в описании RAII обычно забывают написать, что для работы этой техники исключение ОБЯЗАНО быть перехвачено обработчиком исключений этого типа, иначе, если обработчик не будет найден, будет вызвана std::terminate(), которая аварийно завершит выполнение программы. Страуструп описывает это в книге «Язык программирования С++ (03)», глава 14.7.
NB ... unique_ptr (в какой-то степени - читать и дальше) or lock_guard - уже сами справляются с задачами RAII...
-----------
добавлю линк на pdf - про Exception Safety, включая RAII -
Using Exceptions | C++ - using RAII] for cleanup
... вообще Exception Safety - очень интересная тема - очень нравится идея
to prevent exceptions instead of handle exceptions
- достигается в частности использованием идиомы copy-and-swap... - например при создании assignment operator=... таким образом, получает результатом или lhs или нормальный rhs - и никаких exceptions... - красиво, изящно, утончённо
=====
Добавлено через 11 минут
Цитата Сообщение от L0M Посмотреть сообщение
Приведение типа посредством шаблонного метода
CRTP, насколько понимаю... но есть один нюанс - чтобы, действительно, добиться полиморфного поведения от объектов классов, создаваемых на основе шаблона во время компиляции, - т.е. чтобы их можно было помещать в контейнер<T*>, - надо всё-таки ещё создать абстрактный класс, от которого и унаследовать шаблон... - см. Polymorphic copy construction​... поэтому нужен вам или не нужен этот шаблон - это ещё вопрос... хотя заменить динамический полиморфизм статическим (там где это возможно) - вобщем-то неплохо для app performance

Добавлено через 28 минут
=====
Цитата Сообщение от Kastaneda Посмотреть сообщение
pImpl... Тем не менее, за счет дополнительного указателя и вызова функции, вы можете иметь такой уровень абстракции через указатель на реализацию.
спорная трактовка pImpl (точнее перевод на рус.яз.)... обычно private сектор класса можно оформить отдельным классом и использовать указатель на него в основном классе, а подключить его обычным #include impl.h (интерфейс), понятное дело, у которого может быть свой .c (реализация)... такое сокрытие private полей и методов (кстати, понятное дело, что будет больше кода) , действительно, полезно (писать этот ворох кода), когда, например, профилировщик класса показывает, что создание его (например, с большим количеством полей) является узким местом в вашем коде... в частности, при разработке библиотек и тем более кросс-платформенных - разработчику быстрее будет перекомпилировать проект если разделены зависимости между private и public частями основного интерфейса (.h-файл) и изменения внесены только в реализацию скрытого класса... то и не будут перекомпилироваться все пользователи его, а их может быть несколько, в частности при разработке кроссплатформенной версии библиотеки...
в Qt часто используется идиома pImpl для Widget'ов, например, для описания родительского окна и его потомков... в используемом мной U++ это не совсем актуально в таком разрезе, поскольку наследования ЭУ как такового нет (они все независимы - главное правильно расставлять конструкторы и деструкторы, согласно RAII, полагаю)... имхо
и кстати, используя pImpl, - вы теряете возможность использовать inline (хотя последнего современные компиляторы в принципе воспринимают как попало)...
и вижу пользу в использовании идиомы pImpl - например, при создании части интерфейса на C++ с выносом этой части в отдельную реализацию, чтобы основная часть основного класса, написанного на С, могла использовать этот свой stuff на С++ ... - т.к. правила дурного тона - смешивать C и C++ и неуважение к компилятору смешивать языки... имхо
в общем, часто городить pImpl - это просто усложнять себе сопровождаемость кода, но обоснованное использование принесёт свои удобства... имхо

Добавлено через 19 минут
конечно, можно разделить интерфейс и реализацию и с помощью чисто абстрактного класса, но тогда мы вынуждены иметь дело с vtable в run-time'e ... но, используя идиому pImpl и уходя от необходимость иметь дело с vtable в run-time'e, улучшим ли мы performance нашего app, - спорно и не всегда, если нам всё равно приходится выделять доп. память под скрытый объект в придачу к основному объекту...
+ сокрытие части реализации в др. классе, требует подробного описания возможных побочных эффектов этой скрытой части на основной класс хотя бы в документации... вот как-то так

Добавлено через 3 минуты
ведь всегда интересно WHEN to use и каковы Последствия
0
Неэпический
18100 / 10687 / 2061
Регистрация: 27.09.2012
Сообщений: 26,907
Записей в блоге: 1
14.02.2021, 12:22 13
Цитата Сообщение от JeyCi Посмотреть сообщение
Почему-то в описании RAII обычно забывают написать, что для работы этой техники исключение ОБЯЗАНО быть перехвачено обработчиком исключений этого типа, иначе, если обработчик не будет найден, будет вызвана std::terminate(), которая аварийно завершит выполнение программы.
Как-то странно не сказать о том, что программа должна быть еще и запущена перед тем, как будет выброшено исключение.
RAII - отдельный от исключений механизм, конечно же всегда и программа должна быть запущена и исключение перехвачено и всё остальное должно быть правильно.

Что касается std::terminate, то он вызывается, но вот остальное зависит от реализации.
http://eel.is/c++draft/except#handle-9
If no matching handler is found, the function std​::​terminate is invoked; whether or not the stack is unwound before this invocation of std​::​terminate is implementation-defined
0
329 / 149 / 33
Регистрация: 29.06.2019
Сообщений: 1,429
14.02.2021, 13:08 14
Цитата Сообщение от Croessmah Посмотреть сообщение
перед тем, как будет выброшено исключение.
ну не стоит же этого забывать - в др. языках всё, что угодно, и само может выскакивать - в более высокоуровневых... а тут в try{} catch(...){} надо не забыть обернуть, чтобы выскочило...
0
Неэпический
18100 / 10687 / 2061
Регистрация: 27.09.2012
Сообщений: 26,907
Записей в блоге: 1
14.02.2021, 14:25 15
Цитата Сообщение от JeyCi Посмотреть сообщение
чтобы выскочило...
Наоборот, чтобы дальше main'а не выскочило.
0
329 / 149 / 33
Регистрация: 29.06.2019
Сообщений: 1,429
14.03.2021, 16:53 16
Цитата Сообщение от JeyCi Посмотреть сообщение
чтобы, действительно, добиться полиморфного поведения от объектов классов, создаваемых на основе шаблона во время компиляции, - т.е. чтобы их можно было помещать в контейнер<T*>,
действительно, как-то разорвано всё на wiki... простое наследование от Астахова - в стиле CRTP с возможностью использовать объекты в полиморфном контейнере - рабочий вариант так:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <iostream>
#include <memory>
#include <vector>
 
using namespace std;
 
// Base class has a pure virtual function for cloning
class AbstractShape {
public:
    virtual ~AbstractShape () = default;
    virtual std::unique_ptr<AbstractShape> clone() const = 0;
     virtual double area() const=0;
};
 
// This CRTP class implements clone() for Derived
template <typename Derived>
class Shape : public AbstractShape {
public:
    // This allows obtaining copies of squares, circles or any other shapes by shapePtr->clone()
    std::unique_ptr<AbstractShape> clone() const override {
        return std::make_unique<Derived>(static_cast<Derived const&>(*this));
    }
 
protected:
   // We make clear Shape class needs to be inherited
   Shape() = default;
   Shape(const Shape&) = default;
   Shape(Shape&&) = default;
  
};
 
// Every derived class inherits from CRTP class instead of abstract class
class Square : public Shape<Square>{
    public:
        Square(double a = 1): _a(a) { };     
         virtual double area() const { return _a * _a; };
 
    private:
        double _a;
};
 
class Circle : public Shape<Circle>{
    public:
        Circle(double r = 1): _r(r) { } ;
        virtual double area() const { return _r * _r * 3.14; };
    private:
        double _r;
};
 
class Rectangle : public Shape<Rectangle>{
    public:
        Rectangle(double a = 1, double b=1): _a(a), _b(b)  { };  
         virtual double area() const { return _a * _b; };
 
    private:
        double _a, _b;
};
 
int main(int argc, const char *argv[])
{
    int a=2;
    {
        Shape<Square>* p=new Square(a); 
        Shape<Circle>* pp=new Circle(a);
        Shape<Rectangle>* ppp=new Rectangle(a, 4);
        
        //unique_ptr<AbstractShape>  sh = p->clone();
        //unique_ptr<AbstractShape>  sh2 = pp->clone();
        
       vector<unique_ptr<AbstractShape>>  v;
       v.push_back(p->clone());
       v.push_back(pp->clone());
       v.push_back(ppp->clone());
            
        for(auto&& el : v)  {
            cout << el->area() << '\n'; 
        }
    }
    return 0;
}
Кликните здесь для просмотра всего текста
при этом передали rowPointer в std::unique_ptr и удалили при выходе из scope...

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

Добавлено через 19 минут
и наблюдения:
Давайте посмотрим на результаты:
CRTP оказался чуть медленнее на MSVC, но чуть быстрее на GCC и LLVM (по сравнению с обычным виртуальным наследованием)
0
фрилансер
5841 / 5368 / 1102
Регистрация: 11.10.2019
Сообщений: 14,336
14.03.2021, 20:26 17
Цитата Сообщение от JeyCi Посмотреть сообщение
Shape<Square>* p=new Square(a);
        Shape<Circle>* pp=new Circle(a);
        Shape<Rectangle>* ppp=new Rectangle(a, 4);
утечка же

Добавлено через 2 минуты
Цитата Сообщение от JeyCi Посмотреть сообщение
for(auto&& el : v)
я бы поопасался тут делать rvalue ссылку
1
329 / 149 / 33
Регистрация: 29.06.2019
Сообщений: 1,429
14.03.2021, 20:36 18
Цитата Сообщение от Алексей1153 Посмотреть сообщение
утечка же
а я думала, что unique_ptr действует, как обёртка, когда помещаем в неё... и в debug на break_point после выхода из scope, вроде, не увидела левых pointers - вот и подумала, что всё норм.? ...
но, полагаю, можно и так
C++
1
2
        std::unique_ptr<Shape<Square>> p(new Square(a));
        std::unique_ptr<Shape<Circle>> pp(new Circle(a));
Добавлено через 1 минуту
Цитата Сообщение от Алексей1153 Посмотреть сообщение
я бы поопасался тут делать rvalue ссылку
наверно, на pointer'ах rvalue ссылку лучше не делать - приму на заметку!... &
0
фрилансер
5841 / 5368 / 1102
Регистрация: 11.10.2019
Сообщений: 14,336
14.03.2021, 20:43 19
JeyCi, да, я слегка ошибся - у тебя запутанная архитектура ) Утечки не будет

но зачем такие пляски, если вместо new сразу создать умный указатель через make_unique? Зачем метод clone?

Добавлено через 54 секунды
Цитата Сообщение от JeyCi Посмотреть сообщение
наверно, на pointer'ах rvalue лучше не делать
в данном цикле нужно просто константную ссылку. А переместиться может не только поинтер, а объект любого класса

Добавлено через 2 минуты
ещё у меня опасение, что мы тут оффтопим
0
329 / 149 / 33
Регистрация: 29.06.2019
Сообщений: 1,429
14.03.2021, 21:05 20
Цитата Сообщение от Алексей1153 Посмотреть сообщение
но зачем такие пляски, если вместо new сразу создать умный указатель через make_unique? Зачем метод clone?
потому что по тому примеру с wiki (куда оставляла линк) - там помимо template'a ещё и Abstract Class - чтобы нормально в полиморфный контейнер входило... в принципе там всё описано по линку, но примеры уж очень разорванные и потому некомпилируемые - поэтому и выложила рабочую версию...
часто CRTP описывается только с template'ом и наследниками - а дальше пляски и не всегда работает, как положено... в частности полиморфное поведение в полиморфном контейнере... ещё потестить можно обычные примеры, но этот про запас не помешает...
Цитата Сообщение от Алексей1153 Посмотреть сообщение
ещё у меня опасение, что мы тут оффтопим
возможно, в любом случае спасибо за ваш view
0
14.03.2021, 21:05
Ответ Создать тему
Опции темы

КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2024, CyberForum.ru