2014-07-08 2 views
6

Я играл с идиомой Pimpl и пожинал всевозможные выгоды от этого. Единственное, чего я не слишком увлечен, это чувство, которое я получаю, когда определяю функции.Можно ли написать гибкий Pimpl в C++?

  • После того, как в заголовке (Р DEF)
  • После того, как в верхней части .cpp (Impl DEF)
  • После того, как в середине .cpp (Impl Impl)
  • После того, как на нижний конец .cpp (P Impl)

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

Вопрос в том, какие эффективные способы подразумевают или шаблоны моих классов таким образом, что если бы я должен был определить новую функцию, мне нужно было бы написать одно явное определение и реализацию, а остальные остаются пространственно близкими к эксплицитам в коде; и если бы я должен был изменить функцию, необходимые изменения были бы как можно меньше?

+3

Это может быть очень ясно для вас, но я боюсь, что мне не ясна , У вас есть примеры кода, чтобы показать неприятности, с которыми вы сталкиваетесь? –

+0

Почему вы вообще определяете эти функции в Impl? Объявите функцию в заголовке, а затем определите ее в «нижнем конце .cpp». –

+1

@WojtekSurowka _'Declare функция в заголовке, '_ Это было бы не очень полезно с помощью идиомы [Pimpl] (http://en.wikipedia.org/wiki/Opaque_pointer), все дело в том, что реализация не является общедоступной в заголовке. –

ответ

4

Давайте постулировать ваш заголовок начинается что-то вроде этого:

class X 
{ 
    public: 
    ...la de dah... 
    private: 
    struct Impl; 
    Impl* p_impl_; 
}; 

Тогда при добавлении функции у вас есть выбор, чтобы сделать:

  1. у вас есть определение функции X элемент реализации логики , ссылаясь на p_impl_-> вещи повсюду, или

  2. return p_impl->same_fn(all_the_args); и kee p логика внутри класса Impl?

Если вы выберете 1., вы получите объявление функции в заголовке и (чуть более грязное, чем обычно) определение в соответствующем файле реализации.

Если вы выберете 2. то вы получите объявление функции в файле заголовка, определение обертывания/пересылки в соответствующем файле реализации и, как минимум, определение в структуре Impl (я, как правило, не определяю функции вне определения класса Impl - это деталь реализации, и интерфейс все равно не является общедоступным).

В этой ситуации вообще нет желательного способа улучшить ситуацию (например, макро-хакерство и дополнительные сценарии генерации кода в процессе сборки могут быть оправданными, но очень редко).


Это не имеет значения, целую кучу, хотя это может быть интересно, что вариация второго подхода заключается в первом реализовать класс, который не использовать Pimpl идиомы (в комплекте с правильной головой и необязательно встроенные функции), вы можете затем обернуть с помощью объекта управления pimpl и переслать ему функции, и таким образом вы сохраняете свободу иметь какой-то код где-нибудь, когда-нибудь решите, что он хочет использовать функциональность, не используя оболочку pimpl , возможно, для повышения производительности/сокращения использования памяти за счет зависимости от перекомпиляции. Вы также можете сделать это, чтобы использовать конкретное создание шаблона без раскрытия кода шаблона.

Чтобы проиллюстрировать этот вариант (в соответствии с просьбой в комментарии), давайте начнем с глупым не-Pimpl class X в своих собственных файлах, а затем создать Pimpl::X оболочки (использование пространства имен и то же имя класса совершенно не обязательно, но облегчает листать код клиента использовать либо, и напоминание - это не значит быть кратким, дело здесь, чтобы версия, не Pimpl быть полезной тоже):

// x.h 
class X 
{ 
    public: 
    int get() const { return n_; } // inline 
    void operator=(int); // out-of-line definition 
    private: 
    int n_; 
}; 

// x.c++ 
#include <x.h> 
void X::operator=(int n) { n_ = n * 2; } 

// x_pimpl.h 
namespace Pimpl 
{ 
    class X 
    { 
     public: 
     X(); 
     X(const X&); 
     ~X(); 
     X& operator=(const X&); 
     int get() const; 
     void operator=(int); 
     private: 
     struct Impl; 
     Impl* p_impl_; 
    }; 
} 

x_pimpl.c++ 
#include <x.h> 
namespace Pimpl 
{ 
    struct X::Impl 
    { 
     ::X x_; 
    }; 

    // the usual handling... 
    X() : p_impl_(new Impl) { } 
    X(const X& rhs) : p_impl(new Impl) { p_impl_->x_ = rhs.p_impl_->x_; } 
    ~X() { delete p_impl_; } 
    X& operator=(const X& rhs) { p_impl_->x_ = rhs.p_impl_->x_; return *this; } 

    // the wrapping... 
    int X::get() const { return p_impl_->x_.get(); } 
    void X::operator=(int n) { p_impl_->x_ = n; } 
} 

Если вы выберете для выше изменения на 2, что делает «реализацию» пригодной для использования сущностью в ее собственном праве, тогда да - вы можете получить 2 объявления и 2 определения, относящиеся к одной функции, но тогда одно из определений будет простая функция обертки/пересылки, которая является только значительно повторяющейся и утомительной, если функции очень короткие и многочисленные, но имеют множество параметров.

+0

Мне действительно нравится идея потенциально независимой реализации. Если вы не возражаете, не могли бы вы расширить свой код для этой опции? Я был бы рад принять это как гибкий прыщик. – AAorris

+0

@AAorris: добавлено ... ура. –

1

Я только собираюсь начать с того, чтобы судить, чтобы убедиться, что я понимаю: вам нравятся преимущества использования pimpl, но не нравится количество кода шаблона при добавлении или изменении функций?

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

  • Вы пишете код только один раз, но читали его много раз, и вы имеют в вашем распоряжении множество возможностей копирования-вставки. Первоначально создание функции - это не большая часть времени, которое вы потратите на этот класс. Компиляция и поддержка - это время, в котором будет потрачено ваше время.
  • Обязательно сохраните API общедоступного класса как можно проще. Чем меньше функций вы используете в публичном API, тем меньше вы должны писать. Вы можете сделать так много функций, как вам нравится в имплатете, и вам нужно их модифицировать.
  • Если вы обнаружите, что меняете API открытого класса много раз, вы можете захотеть слегка отрегулировать свой процесс проектирования. Проведите еще десять минут, просматривая/записывая прецеденты, и вы можете уменьшить свои изменения API на 90%.
3

Нет требования относить объект IMPL к тем же правилам & как объявление объекта в файле .h. Предоставляя переменные-члены публичным (через объявление struct), вам не нужно реализовывать ненужный слой-оболочку. Это в целом безопасно, поскольку только файл .cpp имеет доступ к IMPL.

Рассмотрим следующий код, который достигает преимущества Pimpl идиомы без ненужного дублирования кода:

// x.h 
class X { 
public: 
    X(); 
    ~X(); 

    X(const X&) = delete; 
    X& operator =(const X&) = delete; 

    void set(int val); 
    int get() const; 

private: 
    struct IMPL; 
    IMPL* impl; 
}; 

// x.cpp 
#include "x.h" 

struct X::IMPL { 
    int val; 
}; 


X::X() : impl(new IMPL) {} 

X::~X() { delete impl; } 

void X::set(int val) 
{ 
    impl->val = val; 
} 

int X::get() const 
{ 
    return impl->val; 
} 

// main.cpp 
#include <iostream> 
#include "x.h" 

int main (int, char *[]) 
{ 
    X x; 
    x.set(10); 
    std::cout << x.get() << std::endl; 
    return 0; 
} 
8

Вы могли бы рассмотреть что-то вдоль этих линий:

класса интерфейс к минимуму повторяющиеся объявления. Клиент будет использовать класс PublicImplementation в своем коде.

Pimpl.h

#ifndef PIMPL_H_ 
#define PIMPL_H_ 

#include <memory> // std::unique_ptr 

class Interface 
{ 
public: 
    virtual ~Interface() {} 

    virtual void func_a() = 0; 
    virtual void func_b() = 0; 
}; 

class PublicImplementation 
{ 
    // smart pointer provides exception safety 
    std::unique_ptr<Interface> impl; 

public: 
    PublicImplementation(); 

    // pass-through invoker 
    Interface* operator->() { return impl.get(); } 
}; 

#endif // PIMPL_H_ 

Pimpl.каст

#include "Pimpl.h" 
#include <iostream> 

class PrivateImplementation 
: public Interface 
{ 
public: 

    void func_a() override { std::cout << "a" << '\n'; } 
    void func_b() override { std::cout << "b" << '\n'; } 
}; 

PublicImplementation::PublicImplementation() 
: impl(new PrivateImplementation) 
{ 
} 

И, наконец, это то, что код делает клиент:

main.cpp

#include "Pimpl.h" 

int main() 
{ 
    PublicImplementation pi; // not a pointer 

    pi->func_a(); // pointer semantics 
    pi->func_b(); 
} 
+2

Вы можете использовать 'std :: unique_ptr impl;', чтобы сохранить PublicInterface tidier и обеспечить безопасность исключений. Также это исправит вашу существующую проблему «Правило трех». –

+0

@MattMcNabb Я обновил пример, чтобы включить ваше предложение. – Galik

+0

Чистый и полезный, именно то, что я искал ... большое спасибо – Oneiros

Смежные вопросы