PImpl (Pointer to Implementation)

PImpl (Pointer to Implementation)
Marius.Maximus
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 2196
0

Taka ciekawostka/zagadka, wszystko bardzo podobnie wygląda a kompiluje się tylko jeden wariant.

  • wersja 1
Kopiuj
// MyClass.h  *****************
#pragma once
#include <memory>

class MyClass {
public:
    MyClass();

    void someFunction();

private:
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};

// MyClass.cpp *****************
#include "MyClass.h"
#include <vector>

struct MyClass::Impl {
    std::vector<int32_t> dataIN;
    std::vector<uint8_t> dataOUT;
};

MyClass::MyClass() : pImpl(std::make_unique<Impl>()) {}

void MyClass::someFunction() {
    // Przykładowe użycie
    pImpl->dataIN.push_back(42);
    pImpl->dataOUT.push_back(255);
}

  • wersja 2
Kopiuj
// MyClass.h  *****************
#pragma once
#include <memory>

class MyClass {
public:
    MyClass();
    ~MyClass() = default;

    void someFunction();

private:
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};

// MyClass.cpp *****************
#include "MyClass.h"
#include <vector>

struct MyClass::Impl {
    std::vector<int32_t> dataIN;
    std::vector<uint8_t> dataOUT;
};

MyClass::MyClass() : pImpl(std::make_unique<Impl>()) {}

void MyClass::someFunction() {
    // Przykładowe użycie
    pImpl->dataIN.push_back(42);
    pImpl->dataOUT.push_back(255);
}
  • wersja 3
Kopiuj
// MyClass.h  *****************
#pragma once
#include <memory>

class MyClass {
public:
    MyClass();
    ~MyClass();

    void someFunction();

private:
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};

// MyClass.cpp *****************
#include "MyClass.h"
#include <vector>

struct MyClass::Impl {
    std::vector<int32_t> dataIN;
    std::vector<uint8_t> dataOUT;
};

MyClass::~MyClass() = default;

MyClass::MyClass() : pImpl(std::make_unique<Impl>()) {}

void MyClass::someFunction() {
    // Przykładowe użycie
    pImpl->dataIN.push_back(42);
    pImpl->dataOUT.push_back(255);
}

😄

MarekR22
  • Rejestracja: dni
  • Ostatnio: dni
4

Wszystko się zgadza.
Powodem są domyślnie generowane implementacje w klasie.
Czyli copy/move constructor, destructor, operator przypisania.
Najlepiej widać to w tej tabeli:
screenshot-20250205112038.png
Wszystko co w tej tabelce jest "defaulted" może prowadzić do tego problemu.

Jeśli deklaracja klasy nie deklaruje lub w jakiś sposób nie blokuje jednej z tych funkcji, to w innych translation units (w cpp które inkludują ten nagłówek).
Może nastąpić próba syntetyzowania tych funkcji, która się nie powiedzie z powodu tego, że MyClass::Impl jest tylko zadeklarowana (incomplete).

przykładowo: ~MyClass() = default; jest problemem, ponieważ to oznacza, że każdy translation unit powinien próbować syntetyzować ten destruktor. A tylko translation unit z pełną definicję tek klasy zna implementację MyClass::Impl

Wersja 3, jest najbliższa poprawności. Jak ktoś spróbuje zrobić move assign, to problem powróci.
Ergo dodałbym:

Kopiuj
class MyClass {
public:
    MyClass();
    ~MyClass();

    MyClass(const MyClass&) = delete;
    MyClass(MyClass&&) = delete; // opcjonalne bo linijka wyżej powoduje usunięcie move constructor.
    MyClass& operator=(const MyClass&) = delete;
    MyClass& operator=(MyClass&&) = delete;

    void someFunction();

private:
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};
mwl4
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Wrocław
  • Postów: 404
1

Generalnie, aby trochę uprościć kod, możesz przyjąć pewne założenia.
Po pierwsze, wskaźnik w obiekcie musi wskazywać na obiekt tak długo, jak obiekt żyje. Więc zawsze możesz utworzyć obiekt implementacji w konstruktorze i niszczyć obiekt wraz ze zniszczeniem obiektu klasy.
Po drugie, metody klasy szablonowej są syntetyzowane dopiero w momencie użycia. To się łączy z puntem trzecim.
Po trzecie, API klasy wskazuje czy klasa może być copyable i moveable. Tym samym, jeśli mamy headery, które wskazują, że obiekt może być kopiowany, to w DLL muszą istnieć takie symbole wraz z implementacją. Jeśli użytkownik klasy linkuje się do DLL w którym nie ma symboli do np. kopiowania klasy, to linker zgłasza błąd.

Kopiuj
template< typename T >
class PImpl
{
public:
    PImpl() : p( new T() ) {}

    PImpl( const PImpl &other ) : p( new T( *other.p ) ) {}
    
    PImpl( PImpl &&other ) : p( new T( std::move( *other.p ) ) ) {}
    
    ~PImpl()
    {
        delete p;
    }

    PImpl &operator=( const PImpl &other )
    {
        *p = *other.p;
        return *this;
    }

    PImpl &operator=( PImpl &&other )
    {
        *p = std::move( *other.p );
        return *this;
    }

    T *operator->() const { return p; }

private:
    T *p;
};

Użycie:

Kopiuj
// MyClass.h

class MyClass
{
public:
    MyClass();
    MyClass( const MyClass & );
    ~MyClass();

    void setSomething( int something );
    int getSomething();

private:
    class Impl;
    PImpl<Impl> pImpl;
};
Kopiuj
// MyClass.cpp

#include "MyClass.h"

class MyClass::Impl
{
public:
    int something;
};

MyClass::MyClass() = default;
MyClass::MyClass( const MyClass & ) = default;
MyClass::~MyClass() = default;

void MyClass::setSomething( int something )
{
    pImpl->something = something;
}

int MyClass::getSomething()
{
    return pImpl->something;
}
Kopiuj
// main.cpp

#include "MyClass.h"

#include <cstdio>

int main()
{
    MyClass obj;
    obj.setSomething( 10 );

    MyClass obj2 = obj;

    printf( "obj2.something = %d\n", obj2.getSomething() );
}
Kopiuj
Program stdout
obj2.something = 10

Jeśli w podanym przykładzie usuniemy MyClass( const MyClass & ); to kod się nie kompiluje, ponieważ klasa nie ma opcji kopiowania.

Bardziej zaawansowany przykład to kiedy w implementacji mamy np. std::unique_ptr i chcemy przenieść obiekt: https://godbolt.org/z/K9G7f44so

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.