C++ идиома pimpl

Вопросы написания собственного программного кода (на любых языках)

Модератор: Olej

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

C++ идиома pimpl

Непрочитанное сообщение Olej » 29 май 2021, 11:15

Идиома pimpl в C++: Указатель на реализацию
19.09.2016
Идиома pimpl (pointer to implementation - указатель на реализацию) полезна в тех случаях, когда нам нужно что-то скрыть. Она обеспечивает еще более глубокий вид инкапсуляции, которая маскирует не просто реализацию, а также все ее зависимости.
Шаблон программирования Pimpl - то, что вам следует знать
Основы
Вы можете встретить шаблон Pimpl под другими именами: d-pointer, compiler firewall или даже шаблон Cheshire Cat или непрозрачный указатель.

В его основной форме шаблон выглядит следующим образом:
В классе мы перемещаем все закрытые члены в новый объявленный тип, например, в класс PrivateImpl .
Объявляем PrivateImpl в заголовочном файле основного класса.
В соответствующем файле cpp объявляем класс PrivateImpl и определяем его.
Теперь, если вы измените закрытую реализацию, код клиента не будет перекомпилирован (поскольку интерфейс не изменился).
Перевод статьи «Pimp My Pimpl», часть 1
В первой части статьи рассматривается классическая идиома Pimpl (pointer-to-implementation, указатель на реализацию), показываются её преимущества и рассматривается дальнейшее развитие идиом на её основе.
Вторая часть будет сосредоточена на том, как уменьшить недостатки, которые неизбежно возникают при использовании Pimpl.

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: C++ идиома pimpl

Непрочитанное сообщение Olej » 25 июн 2021, 15:28

Olej писал(а):
29 май 2021, 11:15
Идиома pimpl (pointer to implementation - указатель на реализацию) полезна в тех случаях, когда нам нужно что-то скрыть.
Делаем (полезный) демонстрационный пример:
- вычисление корня квадратного из числа (что может быть проще?)...
- но вычисление не одним методом, а для сравнения несколькими...
- в том числе и при сравнении времени (производительности) затрачиваемого на вычисление

Код: Выделить всё

#include <iostream>
#include <sstream>
#include <chrono>
#include <math.h>
#include "sqr.h"
using namespace std;

int main() {
    const int rept = 1000000;
    while (true) {
	std::cout << "число [>0] (Enter - завершение): " << std::flush;
	std::string s;
	std::getline( cin, s );
	if (s.empty())
	    break;
	float val = 0;
	std::istringstream( s )  >> val;
	if (0 == val)
	{
	    std::cout << "это не число" << std::endl;
	    continue;
	}
	else if (val < 0)
	{
	    std::cout << "только положительные" << std::endl;
	    continue;
	}
	std::cout << "задан аргумент: " << val << std::endl;
	long double etalon = sqrtl(val), real;
	std::cout << "эталонный результат = " << etalon << std::endl;
	while (true) {
	    std::cout << "метод [0.." << sqr::max_method() << "] (Enter - завершение методов): " << std::flush;
	    std::string s;
	    std::getline( cin, s );
	    if (s.empty())
		break;
	    int m;
	    std::istringstream( s )  >> m;
	    sqr obj( m );
	    std::cout << "метод: " << obj.get_title() << std::endl;
	    real = (long double)obj.calculate(val);
	    std::cout << "SQRT(" << val << ") = " << real << " [" << ((real - etalon) / etalon * 100) << "%]" << std::endl;
	    auto begin = std::chrono::steady_clock::now();
	    for( int i = 0; i < rept; i++ )
		obj.calculate(val);
	    auto end = std::chrono::steady_clock::now();
	    auto elapsed_mls = std::chrono::duration_cast<std::chrono::nanoseconds>(end - begin);
	    std::cout << "время " << rept << " вычислений [мксек] = " << elapsed_mls.count() / 1000 << "." << elapsed_mls.count() % 1000 << std::endl;

	}
    }
}
Программа знает (видит) только публичный интерфейс sqr.h ... но там нет деталей реализации.
Вложения
p2sqr.cc
(1.67 КБ) 44 скачивания

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: C++ идиома pimpl

Непрочитанное сообщение Olej » 25 июн 2021, 15:34

Olej писал(а):
25 июн 2021, 15:28
Программа знает (видит) только публичный интерфейс sqr.h ... но там нет деталей реализации.

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ cat sqr.h 
#include <string>
#include <vector>

class sqrimpl;

class sqr
{
public:
    sqr(int);                       // метод вычисления
    static int max_method(void);    // max. индекс метода: 0...max_method 
    const std::string& get_title(); // название метода
    float calculate(float);         // вычмслить SQRT(...)
private:
    sqrimpl* impl;
};

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ cat sqr.cc 
#include "sqr.h"
#include "sqrimpl.h"

sqr::sqr(int m) : impl(sqrimpl::get_impl(m)) {}

int sqr::max_method(void) {return sqrimpl::max_method();}

const std::string& sqr::get_title() {return impl->get_title();}

float sqr::calculate(float val) {return impl->calculate(val);}
Но и здесь только описание публичного интерфейса (доступного приложениям), но нет никаких деталей реализации.
Все запросы к методам класса sqr транзитом ретранслируются в класс sqrimpl, про который мы видим только его название и наличие указателя на такой объект.
Вложения
sqr.h
(399 байт) 42 скачивания
sqr.cc
(275 байт) 44 скачивания

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: C++ идиома pimpl

Непрочитанное сообщение Olej » 25 июн 2021, 15:40

Olej писал(а):
25 июн 2021, 15:34
Все запросы к методам класса sqr транзитом ретранслируются в класс sqrimpl, про который мы видим только его название и наличие указателя на такой объект.
А вот здесь уже детали реализации:

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ cat sqrimpl.h
#include <string>

class sqrimpl
{
public:
    sqrimpl(const char*);
    static sqrimpl* get_impl(int m);
    static int max_method(void);
    virtual float calculate(float) = 0;
    virtual const std::string& get_title() const;
protected:
    std::string title;
private:
    static sqrimpl* aimpl[];
};

//--------------------------------------------------------------------------------------------------------------------

class sqrimpl1 : public sqrimpl
{
public:
    sqrimpl1(const char* name) : sqrimpl(name) {}
protected:
    float calculate(float);
};

class sqrimpl2 : public sqrimpl
{
public:
    sqrimpl2(const char* name) : sqrimpl(name) {}
protected:
    float calculate(float);
};

class sqrimpl3 : public sqrimpl
{
public:
    sqrimpl3(const char* name) : sqrimpl(name) {}
protected:
    float calculate(float);
};

class sqrimpl4 : public sqrimpl
{
public:
    sqrimpl4(const char* name) : sqrimpl(name) {}
protected:
    float calculate(float);
};

class sqrimpl5 : public sqrimpl
{
public:
    sqrimpl5(const char* name) : sqrimpl(name) {}
protected:
    float calculate(float);
};
И, более того, реализация 5 способов ... некоторые из них совсем экзотические...

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ 
olej@R420:~/2021/OWN_TEST.codes/p2sqr$ 
olej@R420:~/2021/OWN_TEST.codes/p2sqr$ cat sqrimpl.cc
#include <math.h>
#include <cmath>
#include "sqrimpl.h"

//---------------------------------------------------------------------------------------------------------------------------

sqrimpl* sqrimpl::aimpl[] = {
   new sqrimpl1("POSIX C API"), 
   new sqrimpl2("C++ API"), 
   new sqrimpl3("Fast Inversion"), 
   new sqrimpl4("exp (...ln)"),
   new sqrimpl5("pow 1/2 ..."),
};

sqrimpl::sqrimpl(const char* ttl) {
    title = std::string(ttl);
}

sqrimpl* sqrimpl::get_impl(int m)
{
    if (m < 0 ) m = 0;
    if ((unsigned)m >= sizeof(sqrimpl::aimpl) / sizeof(sqrimpl::aimpl[0])) m = 0;
    return aimpl[m];
}

int sqrimpl::max_method(void)
{
    return sizeof(sqrimpl::aimpl) / sizeof(sqrimpl::aimpl[0]) - 1;
}

const std::string& sqrimpl::get_title() const
{
    return title;
}

//---------------------------------------------------------------------------------------------------------------------------

float sqrimpl1::calculate(float x) { return ::sqrt(x); }

//---------------------------------------------------------------------------------------------------------------------------

float sqrimpl2::calculate(float x) { return std::sqrt(x); }

//---------------------------------------------------------------------------------------------------------------------------

// https://habrahabr.ru/company/infopulse/blog/336110/?utm_campaign=email_digest&utm_source=email_habrahabr&utm_medium=email_week_20170829&utm_content=link2post
float sqrimpl3::calculate(float x) { 
    int i = *(int*)&x;                 // представим биты float в виде целого числа
    i = 0x1fbd1df5 + ( i >> 1 );
    float y = *(float*)&i;
    y = y - ( y * y - x ) / 2. / y;    // одна итерация уточнения (метод Ньютона)
    return y;
}

//---------------------------------------------------------------------------------------------------------------------------

float sqrimpl4::calculate(float x) {
    return ::exp(0.5 * ::log(x));
}

//---------------------------------------------------------------------------------------------------------------------------

float sqrimpl5::calculate(float x) {
    return ::pow(x, 0.5);
}

//---------------------------------------------------------------------------------------------------------------------------
... экзотические - как, например, метод sqrimpl2, который раньше уже обсуждался здесь: алгоритмические трюки.
Вложения
sqrimpl.h
(1.07 КБ) 42 скачивания
sqrimpl.cc
(2.26 КБ) 42 скачивания

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: C++ идиома pimpl

Непрочитанное сообщение Olej » 25 июн 2021, 15:43

Olej писал(а):
25 июн 2021, 15:40
А вот здесь уже детали реализации:
А теперь собираем всё это до кучи:

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ cat Makefile 
CXX += -Wall -std=c++11 -pedantic

all: Makefile p2sqr

%.o: %.cc
	$(CXX) -c $< -o $@

p2sqr:  p2sqr.cc sqr.o sqrimpl.o
	$(CXX) $< sqr.o sqrimpl.o -o $@
	rm *.o

clean:
	rm -f $(TASK) *.o

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ make 
g++ -Wall -std=c++11 -pedantic -c sqr.cc -o sqr.o
g++ -Wall -std=c++11 -pedantic -c sqrimpl.cc -o sqrimpl.o
g++ -Wall -std=c++11 -pedantic p2sqr.cc sqr.o sqrimpl.o -o p2sqr
rm *.o

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ ./p2sqr
число [>0] (Enter - завершение): 1000000
задан аргумент: 1e+06
эталонный результат = 1000
метод [0..4] (Enter - завершение методов): 0
метод: POSIX C API
SQRT(1e+06) = 1000 [0%]
время 1000000 вычислений [мсек] = 18216.309
метод [0..4] (Enter - завершение методов): 1
метод: C++ API
SQRT(1e+06) = 1000 [0%]
время 1000000 вычислений [мсек] = 15577.270
метод [0..4] (Enter - завершение методов): 2
метод: Fast Inversion
SQRT(1e+06) = 1000.06 [0.00640259%]
время 1000000 вычислений [мсек] = 21773.480
метод [0..4] (Enter - завершение методов): 3
метод: exp (...ln)
SQRT(1e+06) = 1000 [1.2207e-05%]
время 1000000 вычислений [мсек] = 52177.820
метод [0..4] (Enter - завершение методов): 4
метод: pow 1/2 ...
SQRT(1e+06) = 1000 [0%]
время 1000000 вычислений [мсек] = 68135.661
метод [0..4] (Enter - завершение методов):
число [>0] (Enter - завершение):

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ ./p2sqr
число [>0] (Enter - завершение): 2
задан аргумент: 2
эталонный результат = 1.41421
метод [0..4] (Enter - завершение методов): 0
метод: POSIX C API
SQRT(2) = 1.41421 [-1.71143e-06%]
время 1000000 вычислений [мсек] = 18587.227
метод [0..4] (Enter - завершение методов): 1
метод: C++ API
SQRT(2) = 1.41421 [-1.71143e-06%]
время 1000000 вычислений [мсек] = 20240.423
метод [0..4] (Enter - завершение методов): 2
метод: Fast Inversion
SQRT(2) = 1.41557 [0.0957728%]
время 1000000 вычислений [мсек] = 22924.79
метод [0..4] (Enter - завершение методов): 3
метод: exp (...ln)
SQRT(2) = 1.41421 [-1.71143e-06%]
время 1000000 вычислений [мсек] = 52287.877
метод [0..4] (Enter - завершение методов): 4
метод: pow 1/2 ...
SQRT(2) = 1.41421 [-1.71143e-06%]
время 1000000 вычислений [мсек] = 65192.608
метод [0..4] (Enter - завершение методов):
число [>0] (Enter - завершение):

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: C++ идиома pimpl

Непрочитанное сообщение Olej » 25 июн 2021, 16:24

Olej писал(а):
25 июн 2021, 15:43
А теперь собираем всё это до кучи:
А вот теперь самое интересное...
Вы можете собрать sqrimpl.cc - в отдельную разделяемую (DLL) библиотеку ... (или статическую) ... абсолютно из тех же всех исходных кодов (без изменений), только используя новый Makefile:

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ cat Makefile.dll 
PROG = p2sqr
LNAME = sqr
LIB = lib$(LNAME).so

CXX += -Wall -std=c++11 -pedantic

all: $(LIB) $(PROG)

%.o: %.cc
	$(CXX) -c $< -o $@

$(LIB): sqrimpl.cc sqrimpl.h
	$(CXX) -c -fpic -fPIC -shared $< -o sqrimpl.o
	$(CXX) -shared -o $(LIB) sqrimpl.o 
	rm -f sqrimpl.o

$(PROG): $(PROG).cc sqr.o $(LIB)
	$(CXX) $< sqr.o -Bdynamic -L./ -l$(LNAME) -o $@

clean:
	rm -f $(PROG) $(LIB) *.o
Сборка:

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ make -f Makefile.dll clean
rm -f p2sqr libsqr.so *.o

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ make -f Makefile.dll
g++ -Wall -std=c++11 -pedantic -c -fpic -fPIC -shared sqrimpl.cc -o sqrimpl.o
g++ -Wall -std=c++11 -pedantic -shared -o libsqr.so sqrimpl.o
rm -f sqrimpl.o
g++ -Wall -std=c++11 -pedantic -c sqr.cc -o sqr.o
g++ -Wall -std=c++11 -pedantic p2sqr.cc sqr.o -Bdynamic -L./ -lsqr -o p2sqr
И вообще не поставлять в комплекте приложения sqrimpl.cc (код реализации), а предоставлять готовую бинарную библиотеку.

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ ls -l lib*
-rwxrwxr-x 1 olej olej 23832 июн 25 16:10 libsqr.so

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ ldd p2sqr
    linux-vdso.so.1 (0x00007ffd6bffe000)
    libsqr.so => /home/olej/2021/OWN_TEST.codes/p2sqr/libsqr.so (0x00007f6396451000)
    libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f6396256000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f6396107000)
    libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f63960ec000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6395efa000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f639645f000)
И всё выполняется так же:

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ export LD_LIBRARY_PATH=`pwd`; ./p2sqr
число [>0] (Enter - завершение): 3
задан аргумент: 3
эталонный результат = 1.73205
метод [0..4] (Enter - завершение методов): 3
метод: exp (...ln)
SQRT(3) = 1.73205 [-1.79482e-06%]
время 1000000 вычислений [мксек] = 55536.555
метод [0..4] (Enter - завершение методов):
число [>0] (Enter - завершение):
Ура!
Сбылась сокровенная мечта выньдаунов: теперь исходный код реализации можно не предоставлять!

:twisted:

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: C++ идиома pimpl

Непрочитанное сообщение Olej » 25 июн 2021, 16:48

Olej писал(а):
25 июн 2021, 16:24
(или статическую)
Опять только меняем Makefile:

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ 
cat Makefile.sta 
PROG = p2sqr
LNAME = sqr
LIB = lib$(LNAME).a

CXX += -Wall -std=c++11 -pedantic

all: $(LIB) $(PROG)

%.o: %.cc
	$(CXX) -c $< -o $@

$(LIB): sqrimpl.o
	ar -q $(LIB) $<
	rm -f $<
	ar -t $(LIB)

$(PROG): $(PROG).cc sqr.o $(LIB)
	$(CXX) $< sqr.o -Bstatic -L./ -l$(LNAME) -o $@

clean:
	rm -f $(PROG) $(LIB) *.o *.a
Сборка:

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ make -f Makefile.sta
g++ -Wall -std=c++11 -pedantic -c sqrimpl.cc -o sqrimpl.o
ar -q libsqr.a sqrimpl.o
rm -f sqrimpl.o
ar -t libsqr.a
sqrimpl.o
sqrimpl.o
g++ -Wall -std=c++11 -pedantic -c sqr.cc -o sqr.o
g++ -Wall -std=c++11 -pedantic p2sqr.cc sqr.o -Bstatic -L./ -lsqr -o p2sqr

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ ls -l *.a
-rw-rw-r-- 1 olej olej 38216 июн 25 16:35 libsqr.a

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ ldd p2sqr
	linux-vdso.so.1 (0x00007ffe299eb000)
	libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f4acd67d000)
	libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f4acd52e000)
	libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f4acd513000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4acd321000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f4acd881000)
Выполнение:

Код: Выделить всё

olej@R420:~/2021/OWN_TEST.codes/p2sqr$ ./p2sqr
число [>0] (Enter - завершение): 7
задан аргумент: 7
эталонный результат = 2.64575
метод [0..4] (Enter - завершение методов): 3
метод: exp (...ln)
SQRT(7) = 2.64575 [-2.76652e-06%]
время 1000000 вычислений [мксек] = 53245.296
метод [0..4] (Enter - завершение методов): 
число [>0] (Enter - завершение): 

Ответить

Вернуться в «Программирование»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 11 гостей