Форум программистов «Весельчак У»
  *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

  • Рекомендуем проверить настройки временной зоны в вашем профиле (страница "Внешний вид форума", пункт "Часовой пояс:").
  • У нас больше нет рассылок. Если вам приходят письма от наших бывших рассылок mail.ru и subscribe.ru, то знайте, что это не мы рассылаем.
   Начало  
Наши сайты
Помощь Поиск Календарь Почта Войти Регистрация  
 
Страниц: [1]   Вниз
  Печать  
Автор Тема: Шаблоны и PImpl  (Прочитано 15408 раз)
0 Пользователей и 1 Гость смотрят эту тему.
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« : 03-05-2013 17:01 » 

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

Одним из них является PImpl (pointer to implementation) - способ и скрыть от пользователя особенности реализации того или иного класса, и, что гораздо важнее, хоть как-то упорядочить декларационные зависимости между элементами, не перегружая заголовочный файл и пространство имён всякими "потрохами" классов.

Идея предельно простая:

В заголовочном файле, допустим, header.h, пишем
Код: (C++)
#ifndef HEADER_H
#define HEADER_H

class C
{
private:
    class Implementation;
    Implementation *implementation;
public:
    C();
    void f();
    ~C();
};

#endif
здесь только продекларировано существование класса Implementation, и поскольку нет обращений к его членам, а есть только указатель (размер которого в памяти не зависит от типа), компилятор это успешно переваривает.

В файле реализации, допустим, code.cpp, пишем
Код: (C++)
#include<iostream>
#include "header.h"

using namespace std;

class C::Implementation
{
public:
    void f()
    {
        wcout << L"Hello world" << endl;
    }
};

C::C() :
    implementation(NULL)
{
    this->implementation = new C::Implementation();
}

void C::f()
{
    this->implementation->f();
}

C::~C()
{
    delete this->implementation
}
Разумеется, преимущества, связанные с полной инкапсуляцией реализации класса от пользователя, оборачиваются недостатком обязательного создания объекта в куче и рисками утечек памяти при некорректном завершении жизни объекта, а также дополнительными вызовами обёрточных функций. Ну и ко всему прочему приходится писать дополнительный обёрточный код, который всегда увеличивает трудозатраты на внесение изменений (по сравнению с Java и C#).

В вызывающем коде класс C выглядит и используется обычным образом
Код: (C++)
#include "header.h"

int main()
{
    C c;
    c.f(); // Hello world.
    return 0;
}


Положение становится скверным, когда класс является параметризируемым - шаблоном (template) в терминах C++. Код такого класса не компилируется до подстановки параметров, и для разных параметров компилятор генерирует разные реализации параметризированного класса. По этой причине весь код реализации шаблона обязан находиться в заголовочном файле, чтобы компилятор в момент определения всех параметров мог сгенерировать новый класс по шаблону (для старых стандартов до C++11).
Код: (C++)
#ifndef HEADER_H
#define HEADER_H

#include <iostream>
#include <string>

using namespace std;

template<typename T>
class C
{
public:
    void Process(T &item)
    {
        wcout << item << endl;
    }
};

#endif

На первый взгляд это с PImpl несовместимо. Однако в C++ есть ещё и специализации шаблонов: заранее определённые программистом версии параметризированного класса с конкретными значениями параметров.

Код: (C++)
template<>
class C<wstring>
{
public:
    void Process(wstring &item)
    {
        wcout << item << endl;
    }
};

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

Заголовочный файл header.h
Код: (C++)
#ifndef HEADER_H
#define HEADER_H

#include <cstdlib>

template<typename T>
class Class
{

private:

        class Implementation;

        Implementation *implementation;

public:

        Class();
        void Process(T &item);
        ~Class();

};

#endif
Здесь мы определяем шаблон класса C, но реализация его методов будет специализированной под конкретные типы, и поэтому может быть вынесена из заголовочного файла. Для всех типов, не входящих в перечень специализаций, будет возникать ошибка компиляции. Внутри определяем класс Implementation, чтобы на не него распространялось действие параметра T, поскольку Implementation - тоже элемент специализации. Определение снаружи потребует template-выражения.

Файл реализации code.cpp
Код: (C++)
#include <iostream>
#include <string>
#include "header.h"

using namespace std;

template<typename T>
class Class<T>::Implementation
{

public:

        void Process(T &item)
        {
                wcout << item << endl;
        }

};

#define SPECIALIZATION(T)                                                       \
                                                                                \
        Class<T>::Class() :                                                         \
                implementation(NULL)                                                    \
        {                                                                           \
                this->implementation = new Class<T>::Implementation();                  \
        }                                                                           \
                                                                                \
        void Class<T>::Process(T &item)                                             \
        {                                                                           \
                this->implementation->Process(item);                                    \
        }                                                                           \
                                                                                \
        Class<T>::~Class()                                                          \
        {                                                                           \
                delete this->implementation;                                            \
        }                                                                           \

SPECIALIZATION(wstring)
SPECIALIZATION(int)
SPECIALIZATION(double)

#undef SPECIALIZATION
Здесь мы вынуждены реализовать все варианты каждого метода класса C, чтобы получились специализации для каждого типа. Чтобы не заниматься copy-paste вручную, разумно использовать макрос. А класс Implementation мы можем оформить в виде шаблона, вызываемого из каждой специализации - для каждой специализации будет автоматически создан собственный вариант.

Код вызывающей программы по-прежнему прост и обыкновенен:
Код: (C++)
#include <iostream>
#include <string>
#include "header.h"

using namespace std;

int main()
{
        {
                wstring x(L"Hello world");
                Class<wstring> object;
                object.Process(x);
        }
        {
                double x = 3.14159;
                Class<double> object;
                object.Process(x);
        }
        /* Тут будет ошибка - нет специализации для float.
        {
                float x = 3.14159;
                Class<float> object;
                object.Process(x);
        }
        */

        wcin.get();
        return 0;
}
« Последнее редактирование: 03-05-2013 17:09 от Dimka » Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Антон (LogRus)
Глобальный модератор

ru
Offline Offline
Пол: Мужской
Внимание! Люблю сахар в кубиках!


WWW
« Ответ #1 : 14-11-2013 16:31 » 

Dimka, а может вместо специализации, сделать явное инстанцирование с нужным типом?
Записан

Странно всё это....
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #2 : 14-11-2013 17:57 » 

Антон (LogRus), инстанцирование кого именно и каким конкретно способом?
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
sss
Специалист

ru
Offline Offline

« Ответ #3 : 15-11-2013 07:22 » 

Dimka, вот такое мне удалось скомпилить..

Код:
class A
{
  class B
  {
    public:
    char fB();
  };
  B b;
  char fA() { return b.fB();};
};

char A::B::fB()
{
  return 'B';
}

В других случаях вы  будете описывать класс не внутренний для A.
Записан

while (8==8)
Антон (LogRus)
Глобальный модератор

ru
Offline Offline
Пол: Мужской
Внимание! Люблю сахар в кубиках!


WWW
« Ответ #4 : 15-11-2013 07:51 » 

Dimka, обычным Улыбаюсь явным инстанцированием.
Код: (C++)
template<typename T>
class Class<T>::Implementation
{
   
public:
   
    void Process(T &item)
    {
        wcout << item << endl;
    }
   
};

template<typename T>
Class<T>::Class()
    : implementation(new Implementation())
    {}
template<typename T>
void Class<T>::Process(T&item) {
    implementation->Process(item);
}
template<typename T> Class<T>::~Class() {}

template class Class<wstring>;
template class Class<double>;
template class Class<int>;

это я к тому, что если тебе не нужна специализация, то и макрос тебе твой не нужен.
Записан

Странно всё это....
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #5 : 15-11-2013 11:01 » 

sss, это о чём и к чему?

Антон (LogRus), методы

Код:
template<typename T>
Class<T>::Class()
    : implementation(new Implementation())
    {}
template<typename T>
void Class<T>::Process(T&item) {
    implementation->Process(item);
}
template<typename T> Class<T>::~Class() {}

по старым стандартам C++, запрещающим раздельную компиляцию шаблонов, должны находиться в заголовочном файле. А значит там же должно находиться определение класса Implementation. И таким образом весь PImpl коту под хвост.
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Антон (LogRus)
Глобальный модератор

ru
Offline Offline
Пол: Мужской
Внимание! Люблю сахар в кубиках!


WWW
« Ответ #6 : 15-11-2013 11:41 » 

по старым стандартам C++, запрещающим раздельную компиляцию шаблонов, должны находиться в заголовочном файле. А значит там же должно находиться определение класса Implementation. И таким образом весь PImpl коту под хвост.

почему запрещают? 9 лет этой штукой пользуюсь, не разу не запрещали. Они говорили - "окей, парниша, ты сам знаешь, что делаешь. ну и про грабли тоже знаешь."
пруф в виде книги - изданной в 2002 году - C++ Templates: The Complete Guide
http://www.josuttis.com/tmplbook/
http://www.ozon.ru/context/detail/id/3960662/
Записан

Странно всё это....
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #7 : 15-11-2013 12:24 » 

Антон (LogRus), книги книгами, а ты возьми и напиши законченный работающий пример для Visual Studio 2008 или более ранней, причём чтобы PImpl с шаблоном был в static library, поставляемой как lib + h файлы, а использующий его код в другом проекте. Тогда будет предметное замечание и усовершенствование.
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Антон (LogRus)
Глобальный модератор

ru
Offline Offline
Пол: Мужской
Внимание! Люблю сахар в кубиках!


WWW
« Ответ #8 : 15-11-2013 12:32 » 

я это писал на VS2003 и gcc 3.2, на старой работе, там было ОЧЕНЬ много шаблонов и куча явных инстанцирований, иначе и быть не могло, если мы делали без этого, то падала VS из-за переполнения числа объектов на один obj файл, поэтоми пили отдельно h (объявление) и cpp (реализация) с шаблонами, а в cpp делали явное инстанцирование.

сейчас проверит не могу, под рукой только gcc 4.7
Записан

Странно всё это....
sss
Специалист

ru
Offline Offline

« Ответ #9 : 17-11-2013 07:55 » 

sss, это о чём и к чему?

Я к тому, что определение класса внутри класса можно провести в .h файле, а реализацию вынести в .cpp не загромождая
объявление... При этом можно использовать статический мембер. Всё остальное - горы идиом, которые скрывают чего же
Вы все таки хотите, позволяя в последующем продолжать полемику.
 
Записан

while (8==8)
Dimka
Деятель
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #10 : 17-11-2013 13:23 » 

sss, полемику о чём?

Цитата: sss
Я к тому, что определение класса внутри класса можно провести в .h файле, а реализацию вынести в .cpp не загромождая
объявление...
В этом нет смысла - городить огород. С тем же успехом можно было вообще не делать вложенный класс. Смысл PImpl в другом: чтобы в заголовочном файле не было определения используемого класса.

Типичное использование PImpl - инкапсуляция сторонних библиотек.

Например, мне нужно зачем-то использовать WinAPI, дескриптор окна - тип HWND. Если я его объявлю в классе, в заголовочном файле, пользователь тоже будет вынужден тянуть все заголовки Windows в свои единицы компиляции. Чтобы этого не делать, инкапсулировать в свей библиотеки зависимость от Windows SDK, используется PImpl.

x.h файл
Код: (C++)
#ifndef X_H
#define X_H

// Нет никаких include, никаких зависимостей.

class X
{
private:
  class Impl
  Impl *impl;
public:
  X();
  ~X();
  /* ... */
};

#endif

x.cpp файл
Код: (C++)
#include <windows.h>
#include "x.h"

class X::Impl
{
private:
  HWND hwnd; // использование типов WinAPI скрыто от пользователей
  /* ... */
};

X::X() :
  impl(NULL)
{
  this->impl = new X::Impl();
}

X::~X()
{
  delete this->impl;
}

/* ... */

Теперь я могу дать пользователю h- и lib-файлы, и ему не потребуется устанавливать Windows SDK для сборки своей программы - достаточно иметь runtime для работы (собственно Windows или Wine под Linux и т.п.).
« Последнее редактирование: 18-11-2013 10:44 от Dimka » Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
sss
Специалист

ru
Offline Offline

« Ответ #11 : 18-11-2013 01:48 » 

Понял, извините. В таком случае только указатели..
Записан

while (8==8)
Страниц: [1]   Вверх
  Печать  
 

Powered by SMF 1.1.21 | SMF © 2015, Simple Machines