web-dev-qa-db-ja.com

カスタムコンテナには無料の開始/終了機能が必要ですか?

通常のルールに従って動作するカスタムコンテナクラスを作成する場合(つまり、STLアルゴリズムで動作する、正常に動作するジェネリックコードで動作するなど)、C++ 03ではイテレータサポートとメンバーの開始/終了関数を実装するだけで十分でした。

C++ 11では、範囲ベースのforループとstd :: begin/endの2つの新しい概念が導入されています。範囲ベースのforループは、メンバーの開始/終了関数を理解するため、C++ 03コンテナーは、すぐに使用できる範囲ベースのforをサポートします。アルゴリズムの場合、推奨される方法(HerbSutterによる「最新のC++コードの記述」による)は、メンバー関数の代わりにstd :: beginを使用することです。

ただし、この時点で質問する必要があります-完全修飾begin()関数(つまり、std :: begin(c))を呼び出すか、ADLに依存してbegin(c)を呼び出すための推奨される方法ですか?

この特定のケースではADLは役に立たないようです-可能であればstd :: begin(c)がc.begin()に委任するため、通常のADLの利点は適用されないようです。そして、誰もがADLに依存し始めた場合、すべてのカスタムコンテナーは、必要な名前空間に追加のbegin()/ end()フリー関数を実装する必要があります。ただし、いくつかの情報源は、開始/終了の修飾されていない呼び出しが推奨される方法であることを示唆しているようです(つまり、 https://svn.boost.org/trac/boost/ticket/6357 )。

では、C++ 11の方法は何ですか?コンテナライブラリの作成者は、名前空間stdを使用せずに、修飾されていない開始/終了呼び出しをサポートするために、クラスに追加の開始/終了関数を作成する必要があります。またはstd :: begin;?

59
zeuxcg

いくつかのアプローチがあり、それぞれに長所と短所があります。費用便益分析による以下の3つのアプローチ。

カスタム非メンバーによるADLbegin()/end()

最初の選択肢は、legacy名前空間内に非メンバーのbegin()およびend()関数テンプレートを提供して、必要な機能を、それを提供できる任意のクラスまたはクラステンプレートに後付けしますが、たとえば間違った命名規則。呼び出し元のコードは、ADLに依存してこれらの新しい関数を見つけることができます。サンプルコード(@Xeoによるコメントに基づく):

_// LegacyContainerBeginEnd.h
namespace legacy {

// retro-fitting begin() / end() interface on legacy 
// Container class template with incompatible names         
template<class C> 
auto begin(Container& c) -> decltype(c.legacy_begin())
{ 
    return c.legacy_begin(); 
}

// similarly for begin() taking const&, cbegin(), end(), cend(), etc.

} // namespace legacy

// print.h
template<class C>
void print(C const& c)
{
    // bring into scope to fall back on for types without their own namespace non-member begin()/end()
    using std::begin;
    using std::end;

    // works for Standard Containers, C-style arrays and legacy Containers
    std::copy(begin(c), end(c), std::ostream_iterator<decltype(*begin(c))>(std::cout, " ")); std::cout << "\n";

    // alternative: also works for Standard Containers, C-style arrays and legacy Containers
    for (auto elem: c) std::cout << elem << " "; std::cout << "\n";
}
_

長所完全に一般的に機能する一貫性のある簡潔な呼び出し規約

  • メンバー.begin()および.end()を定義するすべての標準コンテナーおよびユーザータイプで機能します
  • cスタイルの配列で機能します
  • メンバー.begin()を持たないクラステンプレート _legacy::Container<T>_に対して(範囲-forループ!) end()ソースコードの変更を必要としない

短所:使用する必要があります-多くの場所での宣言

  • _std::begin_および_std::end_は、Cスタイルの配列のフォールバックオプションとして、すべての明示的な呼び出しスコープに組み込まれている必要があります(テンプレートヘッダーの潜在的な落とし穴と一般的な迷惑)

カスタムの非メンバーadl_begin()およびadl_end()によるADL

2番目の方法は、非メンバー関数テンプレートadl_begin()adl_end()を提供することにより、前のソリューションのusing宣言を個別のadl名前空間にカプセル化することです。 ADLからも見つけることができます。サンプルコード(@Yakkによるコメントに基づく):

_// LegacyContainerBeginEnd.h 
// as before...

// ADLBeginEnd.h
namespace adl {

using std::begin; // <-- here, because otherwise decltype() will not find it 

template<class C> 
auto adl_begin(C && c) -> decltype(begin(std::forward<C>(c)))
{ 
    // using std::begin; // in C++14 this might work because decltype() is no longer needed
    return begin(std::forward<C>(c)); // try to find non-member, fall back on std::
}

// similary for cbegin(), end(), cend(), etc.

} // namespace adl

using adl::adl_begin; // will be visible in any compilation unit that includes this header

// print.h
# include "ADLBeginEnd.h" // brings adl_begin() and adl_end() into scope

template<class C>
void print(C const& c)
{
    // works for Standard Containers, C-style arrays and legacy Containers
    std::copy(adl_begin(c), adl_end(c), std::ostream_iterator<decltype(*adl_begin(c))>(std::cout, " ")); std::cout << "\n";

    // alternative: also works for Standard Containers, C-style arrays and legacy Containers
    // does not need adl_begin() / adl_end(), but continues to work
    for (auto elem: c) std::cout << elem << " "; std::cout << "\n";
}
_

長所:完全に一般的に機能する一貫した呼び出し規約

  • @Xeoの提案と同じ長所+
  • 繰り返されるusing宣言はカプセル化されています(DRY)

短所:少し冗長

  • adl_begin()/adl_end()begin()/end()ほど簡潔ではありません
  • おそらく慣用的でもありません(明示的ですが)
  • 保留中のC++ 14の戻り値の型の推定は、_std::begin_/_std::end_で名前空間も汚染します

[〜#〜] note [〜#〜]:これが前のアプローチを本当に改善するかどうかはわかりません。

どこでもstd::begin()またはstd::end()を明示的に修飾する

とにかくbegin()/end()の冗長性が放棄されたら、std::begin()/std::end()の修飾された呼び出しに戻ってみませんか?コード例:

_// LegacyIntContainerBeginEnd.h
namespace std {

// retro-fitting begin() / end() interface on legacy IntContainer class 
// with incompatible names         
template<> 
auto begin(legacy::IntContainer& c) -> decltype(c.legacy_begin())
{ 
    return c.legacy_begin(); 
}

// similary for begin() taking const&, cbegin(), end(), cend(), etc.

} // namespace std

// LegacyContainer.h
namespace legacy {

template<class T>
class Container
{
public:
    // YES, DOCUMENT REALLY WELL THAT THE EXISTING CODE IS BEING MODIFIED
    auto begin() -> decltype(legacy_begin()) { return legacy_begin(); }
    auto end() -> decltype(legacy_end()) { return legacy_end(); }

    // rest of existing interface
};

} // namespace legacy

// print.h
template<class C>
void print(C const& c)
{
    // works for Standard Containers, C-style arrays as well as 
    // legacy::IntContainer and legacy::Container<T>
    std::copy(std::begin(c), std::end(c), std::ostream_iterator<decltype(*std::begin(c))>(std::cout, " ")); std::cout << "\n";

    // alternative: also works for Standard Containers, C-style arrays and
    // legacy::IntContainer and legacy::Container<T>
    for (auto elem: c) std::cout << elem << " "; std::cout << "\n";
}
_

長所:ほぼ一般的に機能する一貫した呼び出し規約

  • メンバー.begin()および.end()を定義するすべての標準コンテナーおよびユーザータイプで機能します
  • cスタイルの配列で機能します

短所:少し冗長で後付けは一般的ではなく、保守の問題です

  • std::begin()/std::end()begin()/end()よりも少し冗長です
  • メンバー.begin()を持たないclassLegacyContainerに対してのみ、機能するように改造できます(range-for loops!) __(SOMECODE)の非メンバー関数テンプレートend()およびbegin()の明示的な特殊化を提供することにより、end()(およびソースコードはありません!) __
  • _namespace stdLegacyContainer<T>_のソースコード内にメンバー関数begin()/end()を直接追加することによってのみ、クラステンプレート _LegacyContainer<T>_に後付けできます。テンプレート用に利用可能です)。関数テンプレートを部分的に特殊化できないため、_namespace std_トリックはここでは機能しません。

何を使うの?

コンテナ自体の名前空間内の非メンバーbegin()/end()を介したADLアプローチは、特にレガシークラスとクラステンプレートの改良を必要とする汎用関数の場合、慣用的なC++ 11アプローチです。これは、ユーザーが非メンバーのswap()関数を提供する場合と同じイディオムです。

標準コンテナまたはCスタイルの配列のみを使用するコードの場合、std::begin()およびstd::end()は、using-declarationsを導入せずにどこでも呼び出すことができますが、より詳細な呼び出しが必要になります。このアプローチは後付けすることもできますが、_namespace std_(クラスタイプの場合)またはインプレースソース変更(クラステンプレートの場合)をいじる必要があります。それは可能ですが、メンテナンスの手間をかける価値はありません。

問題のコンテナがコーディング時に認識されている非ジェネリックコードでは、標準コンテナのみをADLに依存し、Cスタイルの配列に対して_std::begin_/_std::end_を明示的に修飾することもできます。呼び出しの一貫性は失われますが、using-declarationsを節約できます。

35
TemplateRex