LoginSignup
2
1

More than 1 year has passed since last update.

[C++] ダウンキャストを回避してサブクラスにアクセスする

Posted at

端的に言うと単なる前方宣言を利用したトリックです。
いないと思いますが、期待された方はごめんなさい。

概要

本記事では、インターフェースBを引数に取るメソッド
virtual IA::Func(const IB* b) = 0;の実装について考えます。
このメソッドの実装A::Func(const IB* b) override;で、IBのサブクラスBから利用しているAPI等のハンドルを取得したいとなったとき、
最も手っ取り早いのはstatic_cast<const B*>(b)でサブクラスへキャストすることでしょう。
コーディング規約など、何らかの理由でこのキャストを回避したい場合を考えてみました。

既存のデザインパターンやイディオムとして存在するかどうか分かりませんでした。
名称や類似パターンをご存じの方是非教えてください。
(何が近いんでしょう?ProxyBridge?)

方法

冒頭で述べた通り、前方宣言を使います。
以下のようにします。

IB.h
struct IB
{
    struct ImplHolder;                                      // この二行。名前は適当
    virtual const ImplHolder& GetImplHolder() const = 0;    // 

    virtual ~IB() {}
    static IB* New();
};

サブクラスのBから参照できる場所で、前方宣言したIB::ImplHolderを定義します。

B.h
#include "IB.h"

struct B;
struct IB::ImplHolder
{
    B& ref;
    explicit ImplHolder(B& b) : ref(b) {}

    B* operator->() const noexcept { return &ref; }  // ポインタのように使えると楽
};

class B : public IB
{
    IB::ImplHolder m_holder;
public:
    virtual const ImplHolder& GetImplHolder() const override;

    uint32_t GetHandle() const;
    B();
};

御覧のように、インターフェース側ではインナークラスImplHolderを定義せず、サブクラス側で定義します。
また、内部にサブクラスへの参照を持つことで、b->GetImplHolder()->GetHandle()のようにしてサブクラスにアクセスすることができます。
利用する側にとってはインナークラスとその取得メソッドがあること位しか分かりません。
B.hは非公開にしておきましょう。

実際にサブクラスAで利用する場合は以下のようになるでしょうか。

A.cpp
void A::Func(const IB* b)
{
    std::cout << "A::Func(const IB* b)" << std::endl;

    auto impl = b->GetImplHolder();
    //auto impl = static_cast<const B*>(b);  // キャストを使う場合

    auto handle = impl->GetHandle();

    std::cout << "handle = " << handle << std::endl;
}

このような小細工を入れておくことで、ダウンキャストを回避しつつ、実際に使う側からはサブクラスA,Bの存在を隠すことができます。

main.cpp
#include "IA.h"
#include "IB.h"

int main()
{
    auto a = IA::New();
    auto b = IB::New();

    a->Func(b);

    delete a;
    delete b;

    return 0;
}

注意点として、サブクラスへの参照を保持する必要があるため、ポインタ一つ分だけサイズが大きくなってしまいます。
64bitだと8byte分増えます(筆者環境)。

実行結果(x86)
A::A() [4byte]
B::B() [8byte]
A::Func(const IB* b)
B::GetHandle()
handle = 4294967295
実行結果(x64)
A::A() [8byte]
B::B() [16byte]
A::Func(const IB* b)
B::GetHandle()
handle = 4294967295

プログラムの全体

本記事の説明用プログラムの全体を載せておきます。

IA.h
#pragma once

struct IB;
struct IA
{
    virtual void Func(const IB* b) = 0;
    virtual ~IA() {}

    static IA* New();
};
IB.h
#pragma once

struct IB
{
    struct ImplHolder;                                      // この二行。名前は適当
    virtual const ImplHolder& GetImplHolder() const = 0;    // 

    virtual ~IB() {}
    static IB* New();
};
A.h
#pragma once
#include "IA.h"

class A : public IA
{
public:
    A();
    virtual void Func(const IB* b) override;
};
B.h
#pragma once
#include "IB.h"

class B;
struct IB::ImplHolder
{
    B& ref;
    explicit ImplHolder(B& b) : ref(b) {}
    B* operator->() const noexcept { return &ref; }  // ポインタのように使えると楽
};

class B : public IB
{
    IB::ImplHolder m_holder;
public:
    virtual const ImplHolder& GetImplHolder() const override;

    B();
    uint32_t GetHandle() const;
};
A.cpp
#include <iostream>
#include "A.h"
#include "B.h"

A::A()
{
    std::cout << "A::A() [" << sizeof(*this) << "byte]" << std::endl;
}

void A::Func(const IB* b)
{
    std::cout << "A::Func(const IB* b)" << std::endl;

    auto impl = b->GetImplHolder();
    //auto impl  = static_cast<const B*>(b);  // キャストを使う場合

    auto handle = impl ->GetHandle();

    std::cout << "handle = " << handle << std::endl;
}

IA* IA::New()
{
    return new A;
}
B.cpp
#include <iostream>
#include "B.h"

const IB::ImplHolder& B::GetImplHolder() const
{
    return m_holder;
}

uint32_t B::GetHandle() const
{
    std::cout << "B::GetHandle()" << std::endl;
    return ~0;  // 適当
}

B::B() : m_holder(*this)
{
    std::cout << "B::B() [" << sizeof(*this) << "byte]" << std::endl;
}

IB* IB::New()
{
    return new B;
}
main.cpp

#include "IA.h"
#include "IB.h"

int main()
{
    auto a = IA::New();
    auto b = IB::New();

    a->Func(b);

    delete a;
    delete b;

    return 0;
}

どんな時に使うのか

VulkanDirectXなどを利用したライブラリを構築する際、利用者側に実装を隠したい場合でしょうか。
例えばIDeviceContext::DrawTexture2D(const ITexture2D* pTex, int x, int y) = 0;を実装したい場合は、
DirectX11を利用するIDeviceContextのサブクラスは、ITexture2DからID3D11ShaderResourceViewなんかを取得したいかと思います。

DX11DeviceContext.cpp
DX11DeviceContext::DrawTexture2D(const ITexture2D* pTex, int x, int y)
{
    auto impl = pTex->GetImplHolder();
    auto srv = impl->GetSRV();
    m_deviceContext->PSSetShaderResources(0, 1, &srv);
    // ...
}

その他

クラス名以外同じコードなのでマクロにしておくといいんじゃないでしょうか。

マクロ

#define INTERFACE                                   \
struct ImplHolder;                                  \
virtual const ImplHolder& GetImplHolder() const = 0;\

#define HOLDER(base, derive)                            \
class derive;                                           \
struct base::ImplHolder                                 \
{                                                       \
    derive& ref;                                        \
    explicit ImplHolder(derive& b) : ref(b) {}          \
                                                        \
    derive* operator->() const noexcept { return &ref; }\
};

#define DERIVED(type)       \
    IB::ImplHolder m_holder;\
public:                     \
    virtual const ImplHolder& GetImplHolder() const override { return m_holder; }

//  struct IB
//  {
//      INTERFACE
//  
//      virtual ~IB() {}
//  
//      static IB* New();
//  };
//  
//  HOLDER(IB, B)
//  
//  class B : public IB
//  {
//      DERIVED(IB)
//  
//      B(); // m_holder の初期化に注意
//      uint32_t GetHandle() const;
//  };

2
1
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1