質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.49%

  • C++

    3576questions

    C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

  • C++11

    108questions

    C++11は2011年に容認されたC++のISO標準です。以前のC++03に代わるもので、中枢の言語の変更・修正、標準ライブラリの拡張・改善を加えたものです。

C++:コンテナをポリモーフィックに枚挙する方法について

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 907

Chironian

C++総合1位

生配列やvector等の要素を枚挙することは多いと思います。
その延長線上で、異なる型のコンテナの要素をポリモーフィズム的に枚挙したいのですが、最近のC++なら簡単と思っていたら、意外に難しいです。

しかし、実は私がまだ把握できていない標準ライブラリやboost等にそのような機能をサポートしたクラスがありそうな気もしています。
そのようなクラスをご存知の方、もしくは、簡単な方法をご存知の方がいましたら、是非、ご教授頂けないでしょうか?

詳細

例えば、下記のようなコンテナaFooArray[]とstd::vector<Bar>があるとします。

struct ElementBase
{
    virutal ~ElementBase() { }
    virtual char const* getName() = 0;
};

struct Foo : public ElementBase
{
    std::string const mName;
    char const* getName()
    {
        return mName.c_str();
    }
    :
};
Foo aFooArray[]={ ... };

struct Bar : public ElementBase
{
    char const* mName;
    char const* getName()
    {
        return mName;
    }
    :
};
std::vector<Bar> aBarVector={ ... };


boost:any的なテクニックでそれぞれへのポインタを管理用コンテナに入れてます。
イメージは下記です。(実際にビルドしてないので雰囲気だけ。)

std::vector<any> aContainerList={any(aFooArray), any(aBarVector)};

そして、下記のようなイメージで枚挙処理を行いたいのです。

for (auto&& element : aContainerList[index].getRange())
{
    std::cout << element.getName() << "\n";
}

たぶん、イテレータにboost:anyテクニックを応用すれば可能と思いますが、コピー時にnewすることになります。イテレータはコピー負荷が軽いことが前提な型っぽい(I/Fの多くが参照渡しではなく値渡し)ので、これは避けたいです。

以前、こちらで質問し、その過程で思いついたものの改造版をQiitaへ投稿しました。
そのRangeクラス内にboost::anyテクニックを使って、異なるコンテナ用イテレータを保持することで対応できそうな気がしているのですが、その前に何か良いアイデアがあればと思い、質問させて頂いています。

自作してみました

yohhoyさんに教えていただいたboostの実装を参考にしつつ、自作してみました。
イテレータではなく範囲ベースfor専用のレンジをboost:any的に保持してます。
範囲ベースfor専用のレンジはQiitaへ投稿したrange-based forをより使いやすくするを改造しました。

後、下記ソースではContainerHolderもboost::anyのようにしてますが、こちらは勿体無い気がしてます。コンテナへの参照をunique_ptr<>で保持してます。わざわざヒープ領域に「参照変数領域」を確保していることになるので、なんか違う気がするのですが、良い方法を思いつきませんでした。

poly_range.h

#ifndef POLY_RANGE_H
#define POLY_RANGE_H

#include <memory>

//----------------------------------------------------------------------------
//      範囲ベースforへ渡すリファレンサー(RBForReferencerの特化)
//----------------------------------------------------------------------------

template<class tPolyRangeBase, class tElementBase>
class PolyReferencer
{
    tPolyRangeBase& mPolyRange;
public:
    PolyReferencer(tPolyRangeBase& iPolyRange) : mPolyRange(iPolyRange)
    { }

    tElementBase& operator*()
    {
        return mPolyRange.mHolder->front();
    }
    tElementBase const& operator*() const
    {
        return mPolyRange.mHolder->front();
    }
    void operator++()
    {
        mPolyRange.mHolder->drop_front();
    }
    bool operator!=(PolyReferencer const& iRhs) const
    {
        if (!mPolyRange.mHolder.get())
    return false;
        return !mPolyRange.mHolder->empty();
    }
};

//----------------------------------------------------------------------------
//      枚挙用Range
//----------------------------------------------------------------------------

template<class tElementBase>
class PolyRange
{

//      ---<<< 基底クラス >>>---

    struct HolderBase
    {
        virtual tElementBase      & front()       = 0;
        virtual tElementBase const& front() const = 0;
        virtual void drop_front() = 0;
        virtual bool empty() const = 0;

        // これがないとmsvc 2015の「反復子のデバッグのサポート」用の
        //  イテレータのデストラクタが呼ばれないため、異常動作する。
        virtual ~HolderBase() { }
    };

//      ---<<< 実クラス >>>---

    template<typename tIterator>
    struct Holder : public HolderBase
    {
        tIterator   mBegin;
        tIterator   mEnd;

        Holder(tIterator&& iBegin, tIterator&& iEnd) :
            mBegin(std::forward<tIterator>(iBegin)),
            mEnd  (std::forward<tIterator>(iEnd))
        { }
        ~Holder() = default;

        tElementBase& front()
        {
            return *mBegin;
        }
        tElementBase const& front() const
        {
            return *mBegin;
        }
        void drop_front()
        {
            ++mBegin;
        }
        bool empty() const
        {
            return mBegin == mEnd;
        }
    };

//      ---<<< 各種設定 >>>---

    template<class, class> friend class PolyReferencer;
    typedef PolyReferencer<PolyRange, tElementBase>   PolyReferencer;

//      ---<<< 記録領域 >>>---

    std::unique_ptr<HolderBase> mHolder;

//      ---<<< コンストラクタ >>>---

public:
    PolyRange() { }

    template<typename tIterator>
    PolyRange(tIterator&& iBegin, tIterator&& iEnd) :
        mHolder
        (
            new Holder<tIterator>
            (
                std::forward<tIterator>(iBegin),
                std::forward<tIterator>(iEnd)
            )
        )
    { }

    PolyReferencer begin() {return PolyReferencer(*this);}
    PolyReferencer end()   {return PolyReferencer(*this);}
};

//----------------------------------------------------------------------------
//      コンテナのホルダ
//          保持しているコンテナの範囲ベースfor専用Rangeを獲得する機能を持つ
//----------------------------------------------------------------------------

template<class tElementBase>
class ContainerHolder
{

//      ---<<< 基底クラス >>>---

    struct HolderBase
    {
        virtual ~HolderBase() { }
        virtual PolyRange<tElementBase> getRange() = 0;
    };

//      ---<<< 実クラス >>>---

    template<class tContainer>
    struct Holder : public HolderBase
    {
        tContainer& mContainer;

        Holder(tContainer& iContainer) : mContainer(iContainer)
        { }
        ~Holder() = default;

        PolyRange<tElementBase> getRange()
        {
            return PolyRange<tElementBase>(std::begin(mContainer), std::end(mContainer));
        }
    };

//      ---<<< 記録領域 >>>---

    std::unique_ptr<HolderBase> mHolder;

//      ---<<< コンストラクタ >>>---

public:
    template<typename tContainer>
    ContainerHolder(tContainer& iContainer) :
        mHolder(new Holder<tContainer>(iContainer))
    { }

    PolyRange<tElementBase> getRange()
    {
        return mHolder->getRange();
    }
};

#endif // POLY_RANGE_H

以下、サンプル・ソースです。
main.cpp

#include <iostream>
#include <vector>
#include <string>
#include "poly_range.h"

// 要素の型
struct ElementBase
{
    virtual ~ElementBase() { }
    virtual char const* getName() = 0;
};

struct Foo : public ElementBase
{
    std::string const mName;
    char const* getName()
    {
        return mName.c_str();
    }

    Foo(std::string const& iName) : mName(iName) { }
};

struct Bar : public ElementBase
{
    char const* mName;
    char const* getName()
    {
        return mName;
    }

    Bar(char const* iName) : mName(iName) { }
};

// コンテナ
Foo gFooArray[]={ {"Foo0"}, {"Foo1"}, {"Foo2"} };
std::vector<Bar> gBarVector={ "Bar0", "Bar1", "Bar2" };

// コンテナのリスト
typedef ContainerHolder<ElementBase>    Container;
std::vector<Container> gContainerList;

int main()
{
    std::size_t aFooArrayIndex=gContainerList.size();
    gContainerList.emplace_back(Container(gFooArray));

    std::size_t aBarVectorIndex=gContainerList.size();
    gContainerList.emplace_back(Container(gBarVector));

    std::cout << "--- FooArray ---\n";
    for (auto&& element : gContainerList[aFooArrayIndex].getRange())
    {
        std::cout << element.getName() << "\n";
    }
    std::cout << "\n";

    std::cout << "--- BarVector ---\n";
    for (auto&& element : gContainerList[aBarVectorIndex].getRange())
    {
        std::cout << element.getName() << "\n";
    }
    std::cout << "\n";

    return 0;
}

【補足】
gContainerListへの値設定時、初期化リストを使おうとしたのでずか、ContainerHolderのコピー・コンストラクタが呼び出され、unique_ptrを使っているので暗黙的にdeleteされており、コンパイル・エラーでした。
push_backやemplace_backではムーブだけでよいので、原因はよく分かりませんでした。
もし、分かる方がいらっしゃいましたら、何故初期化リストではコピー・コンストラクタが必要になるのかご教授頂けると幸いです。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • yohhoy

    2016/05/08 15:23

    elementの型には何を期待するのでしょう?FooとBarには何の相関もないため、動的な(実行時)ポリモーフィズムの場合、何の操作も期待できない気がします。静的(コンパイル時の)ポリモーフィズムが前提ですか?

    キャンセル

  • Chironian

    2016/05/08 17:34

    ああっ、確かにそうです。見落としてました。FooとBarは共通の基底クラスを持つ必要がありますね。質問文を修正します。

    キャンセル

回答 1

checkベストアンサー

+2

要件をイマイチ理解していないので外しているかもしれませんが、Boost.Rangeでは any_range が提供されます。その名前通り"イテレータにboost:anyテクニックを応用"したものですね。

公式ドキュメントにも記載がありますが、Type Erasureを使っているため仮想関数呼び出しコストは存在します。


追記: 追加された疑問に回答します。

何故初期化リストではコピー・コンストラクタが必要になるのか

初期化リストを受け取る側のstd::initializer_list<E>クラステンプレートでは、const修飾された要素型にしかアクセスできないため、C++14現在では要素型Eにはコピー操作を要求します。

ただやはり不便ですし必ずしも必須要件でもないため、N4166: Movable initializer lists で改善提案がされているようです。最新の提案文書(v2)は P0065R0 です。提案文書によれば、C++11(旧C++0x)時点では時間切れでムーブセマンティクスに対応できなかったとのことで、将来的には何らかのサポートがあるかもしれませんね。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/05/09 10:40

    情報、ありがとうございます。

    まず、boost::AnyのようなテクニックのことをType Erasureと呼ぶのですね。やっと理解できました。(以前見た時はまだboost:Anyを理解してなかったので理解できなかったようです。)
    そして、私がやりたいことはRange自体がType Erasureになっている必要が有ります。
    コンテナ要素の型を知らないまま枚挙したいからです。
    boost:any_rangeはテンプレート・パラメータにコンテナの要素の型を与える必要が有るため、残念ながら使えなさそうです。

    思っていたよりレアな要求のようですので、自作しようと思います。
    満足できるものを作れたら、ここにて報告します。

    キャンセル

  • 2016/05/09 19:51

    ちょっと余裕ができたので、boost::any_rangeのソースを追いかけてみました。

    > boost:any_rangeはテンプレート・パラメータにコンテナの要素の型を与える必要が有るため、残念ながら使えなさそうです。

    と見えたのですが、すいません。どうも違うようです。
    コンテナの要素の型を与える必要はありますが、例えば質問に記載したElementBaseを与えてもよさそうです。といいますか、それが本来の使い方っぽいです。

    正に「イテレータにboost:anyテクニックを応用すれば可能と思います」を実現しているもののようです。
    しかし、もしかすると偏見かも知れませんが、関数呼び出しでバラメータとして与えるとコピーされ、その度にnewが走りそうでちょっと躊躇してます。(戻り値側はRVO等でコピーしないと思いますが、関数呼び出し側だと値渡しなのでコピーしそうですし、std::moveしてもムーブしてくれない筈。)

    キャンセル

  • 2016/05/15 12:47

    yohhoyさん。

    追加質問へも回答頂きありがとうございます。

    > 初期化リストを受け取る側のstd::initializer_list<E>クラステンプレートでは、const修飾された要素型にしかアクセスできないため、C++14現在では要素型Eにはコピー操作を要求します。

    なるほど。const参照で受け取っているから、ムーブできないのが現在の仕様なのですね。
    lvalue, rvalueの両方を受け取れるconst参照はstd::forwardに較べて理解しやすいので良く使いますがconstになってしまうのが欠点です。テンプレート+std::forwardを使えば回避できますがこれは中々難しく、実は今回の実装で初めて使うのに成功しました。
    レベルは全然違うと思いますが、const参照よりstd::forwardは難しいという意味では似たような話みたいですね。なるほどです。

    私の根性ではとても調査できそうにない内容でした。本当にありがとうございました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.49%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る

  • C++

    3576questions

    C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

  • C++11

    108questions

    C++11は2011年に容認されたC++のISO標準です。以前のC++03に代わるもので、中枢の言語の変更・修正、標準ライブラリの拡張・改善を加えたものです。