60

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

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

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

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

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

4

1 に答える 1

36

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

カスタム非会員によるADL begin()/end()

最初の代替案は、名前空間内に非メンバーbegin()およびend()関数テンプレートを提供legacyして、必要な機能をそれを提供できる任意のクラスまたはクラス テンプレートに後付けしますが、たとえば間違った命名規則があります。呼び出しコードは、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 スタイルの配列で動作します
  • メンバーを持たず、ソースコードの変更を必要とせずに、任意のクラステンプレートに対して機能するように後付けできます ( range-for ループでも!)。 legacy::Container<T>.begin()end()

短所: 多くの場所で using 宣言が必要です

  • std::beginまたstd::end、C スタイルの配列のフォールバック オプションとして、すべての明示的な呼び出しスコープに持ち込む必要があります (テンプレート ヘッダーの潜在的な落とし穴と一般的な煩わしさ)。

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

adl2 番目の方法は、非メンバー関数テンプレートadl_begin()およびを提供することによって、前のソリューションの using 宣言を別の名前空間にカプセル化するadl_end()ことです。これは、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

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

明示的に修飾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()
  • 非メンバー関数テンプレートの明示的な特殊化と LegacyContainer.begin()end()begin()end()namespace std
  • メンバー関数を直接追加することによってのみ、クラステンプレート に後付けできます/のソースコード内(テンプレート用に利用可能)。関数テンプレートは部分的に特殊化できないため、ここでは このトリックは機能しません。LegacyContainer<T>begin()end()LegacyContainer<T>namespace std

何を使う?

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

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

問題のコンテナーがコーディング時に既知である非汎用コードでは、標準コンテナーのみの ADL に依存し、C スタイルの配列を明示的に修飾std::begin/することもできます。std::end呼び出しの一貫性がいくらか失われますが、using 宣言を節約できます。

于 2013-07-10T07:09:14.280 に答える