20

コンストラクターの実行順序が重要な場合、どうすればstd :: make_tupleを使用できますか?

たとえば、クラスAのコンストラクターとクラスBのコンストラクターの実行順序は、次の場合は未定義だと思います。

std::tuple<A, B> t(std::make_tuple(A(std::cin), B(std::cin)));

質問へのコメントを読んだ後、私はその結論に達しました

std::tupleをテンプレートパラメータパックに変換する

それはこれを言います

template<typename... args>
std::tuple<args...> parse(std::istream &stream) {
  return std::make_tuple(args(stream)...);
}

実装には、コンストラクターの実行順序が定義されていません。

更新し、コンテキストを提供します。

私がやろうとしていることの背景をもう少し説明するために、ここにスケッチを示します。

CodeSynthesis XSDバイナリ解析/シリアル化の助けを借りて、stdinからいくつかのシリアル化されたオブジェクトを読み込みたいです。このような解析とシリアル化がどのように行われるかの例を次に示します。example/cxx/ tree / binary / xdr / driver.cxx

xml_schema::istream<XDR> ixdr (xdr); 
std::auto_ptr<catalog> copy (new catalog (ixdr));

シリアル化されたオブジェクトが持つクラスのリスト(たとえば、カタログ、カタログ、3つのシリアル化されたオブジェクトのsomeOtherSerializableClass)を指定し、その情報をtypedefとして格納できるようにしたい

template <typename... Args>
struct variadic_typedef {};

typedef variadic_typedef<catalog, catalog, someOtherSerializableClass> myTypes;

で提案されているように、テンプレートパラメータパックを展開せずに「保存」することは可能ですか?

解析が終了した後、std::tupleを操作する方法を見つけます。スケッチ:

auto serializedObjects(binaryParse<myTypes>(std::cin));

ここで、serializedObjectsのタイプは

std::tuple<catalog, catalog, someOtherSerializableClass>
4

5 に答える 5

10

自明な解決策は、最初に使用するのではなく、直接構築することです。メンバーのコンストラクターが呼び出される順序は明確に定義されています。std::make_tuple(...)std::tuple<...>

template <typename>
std::istream& dummy(std::istream& in) {
    return in;
}
template <typename... T>
std::tuple<T...> parse(std::istream& in) {
    return std::tuple<T...>(dummy<T>(in)...);
}

関数テンプレートdummy<T>()は、何かを拡張するためにのみ使用されます。順序は、 内の要素の構築順序によって決まりますstd::tuple<T...>

template <typename... T>
    template <typename... U>
    std::tuple<T...>::tuple(U...&& arg)
        : members_(std::forward<U>(arg)...) { // NOTE: pseudo code - the real code is
    }                                        //       somewhat more complex

以下の議論とXeoのコメントに従って、より良い代替手段は使用することです

template <typename... T>
std::tuple<T...> parse(std::istream& in) {
    return std::tuple<T...>{ T(in)... };
}

ブレース初期化子リスト内の引数の評価順序は、それらが出現する順序であるため、ブレース初期化の使用は機能します。のセマンティクスはT{...}、12.6.1 [class.explicit.init] パラグラフ 2 で説明されており、リスト初期化セマンティクスの規則に従うことが述べられています (注: これは同種の型でのみ機能する std::initializer_list とは関係ありません)。順序の制約は、8.5.4 [dcl.init.list] パラグラフ 4 にあります。

于 2012-12-27T15:28:52.003 に答える
3

コメントにあるように、initializer-list を使用できます。

return std::tuple<args...>{args(stream)...};

これは、std::tupleおよびそのようなもの(初期化リストをサポート)で機能します。

しかし、より一般的で、initializer-list を使用できない場合に役立つ別のソリューションを取得しました。それでは、初期化リストを使用せずにこれを解決しましょう。

template<typename... args>
std::tuple<args...> parse(std::istream &stream) {
  return std::make_tuple(args(stream)...);
}

解決策を説明する前に、まず問題について説明したいと思います。実際、問題を段階的に考えることは、最終的に解決策を考え出すのにも役立ちます。したがって、議論(および思考プロセス)を簡単にするために、それargsが3つの異なるタイプ、つまり. XYZ、つまりargs = {X, Y, Z}、これらの線に沿って考え、段階的に解決策に到達することができます。

  • 何よりもまず、関数の引数が評価される順序は C++ 標準では規定されていないため、XY、およびのコンストラクタは任意の順序で実行できます。Z

  • Xしかし、最初に 、次にY、およびを構築したいと考えていZます。または、少なくともその動作をシミュレートしたいと考えています。Xつまり、入力ストリームの先頭にあるデータで構築する必要があり (データが であるとxDataます)、xData のY直後に来るデータで構築する必要があります

  • ご存知のように、 X が最初に構築されるとは限らないため、 を装う必要があります。基本的に、最初に構築されたとしても、ストリームの先頭にあるかのようにストリームからデータを読み取りますが、それは不可能に思えます。入力ストリームZから読み取る限り不可能ですが、 などのインデックス可能な データ構造からデータを読み取る場合は可能です。std::vector

  • したがって、私のソリューションはこれを行います。std::vector最初にデータを入力し、次にすべての引数がこのベクトルからデータを読み取ります。

  • 私のソリューションでは、ストリームの各行に、任意の型のオブジェクトを構築するために必要なすべてのデータが含まれていると想定しています。

コード:

//PARSE FUNCTION 
template<typename... args>
std::tuple<args...> parse(std::istream &stream) 
{
  const int N = sizeof...(args);
  return tuple_maker<args...>().make(stream, typename genseq<N>::type() );
}

そしてtuple_maker、次のように定義されています。

//FRAMEWORK - HELPER ETC

template<int ...>
struct seq {};

template<int M, int ...N>
struct genseq  : genseq<M-1,M-1, N...> {};

template<int ...N>
struct genseq<0,N...>
{
   typedef seq<N...> type;
};

template<typename...args>
struct tuple_maker
{
   template<int ...N>
   std::tuple<args...> make(std::istream & stream, const seq<N...> &)
   {
     return std::make_tuple(args(read_arg<N>(stream))...);
   }
   std::vector<std::string> m_params;
   std::vector<std::unique_ptr<std::stringstream>> m_streams;
   template<int Index>
   std::stringstream & read_arg(std::istream & stream) 
   {
     if ( m_params.empty() )
     {
        std::string line;
        while ( std::getline(stream, line) ) //read all at once!
        {
                m_params.push_back(line);
        }
     }
     auto pstream = new std::stringstream(m_params.at(Index));
     m_streams.push_back(std::unique_ptr<std::stringstream>(pstream));
     return *pstream;
   }
};

テストコード

///TEST CODE

template<int N>
struct A 
{
    std::string data;
    A(std::istream & stream) 
    {
        stream >> data;
    }
    friend std::ostream& operator << (std::ostream & out, A<N> const & a)
    {
        return out << "A" << N << "::data = " << a.data ;
    }
};

//three distinct classes!
typedef A<1> A1; 
typedef A<2> A2;
typedef A<3> A3;

int main()
{
    std::stringstream ss("A1\nA2\nA3\n");
    auto tuple = parse<A1,A2,A3>(ss);
    std::cout << std::get<0>(tuple) << std::endl;
    std::cout << std::get<1>(tuple) << std::endl;
    std::cout << std::get<2>(tuple) << std::endl;
}

出力:

A1::data = A1
A2::data = A2
A3::data = A3

これは期待されています。ideoneでデモをご覧ください。:-)

この解決策は、read_argそれ自体への最初の呼び出しですべての行を読み取ることにより、ストリームからの読み取り順序の問題を回避し、その後のすべての呼び出しstd::vectorはインデックスを使用して から読み取ることに注意してください。

これで、クラスのコンストラクターにいくつかの printf を入れることができます。構築の順序が、関数テンプレートへのテンプレート引数の順序と同じではないparseことを確認できます。これは興味深いことです。また、ここで使用する手法は、リストの初期化を使用できない場所でも役立ちます。

于 2012-12-27T15:43:10.363 に答える
2

ここには特別なことは何もありませんmake_tuple。C++ の関数呼び出しでは、その引数を不特定の順序で呼び出すことができます (コンパイラが自由に最適化できるようにします)。

順序が重要になるような副作用のあるコンストラクターを使用することはお勧めしません (これはメンテナンスの悪夢になります) が、これが絶対に必要な場合は、いつでもオブジェクトを明示的に構築して、必要な順序を設定できます。

A a(std::cin);
std::tuple<A, B> t(std::make_tuple(a, B(std::cin)));
于 2012-12-27T14:27:04.767 に答える
0

この回答は、テンプレート パックの質問に対して行ったコメントからのものです。

構築されたコンポーネントからmake_tupleタプル型を推測し、関数の引数の評価順序が定義されていないため、構築は機械の内部で行われる必要があります。これは私がコメントで提案したものです。その場合、使用する必要はありませんmake_tuple; タプル型から直接タプルを構築できます。しかし、それは構築を命令するものでもありません。ここで行うことは、タプルの各コンポーネントを構築し、コンポーネントへの参照のタプルを構築することです。コンポーネントが簡単に移動またはコピーできる場合、参照のタプルは目的のタイプのタプルに簡単に変換できます。

これが解決策です(コメントのlwsリンクから)わずかに変更され、少し説明されています。このバージョンは、型がすべて異なるタプルのみを処理しますが、理解しやすくなっています。それを正しく行う別のバージョンが下にあります。オリジナルと同様に、タプル コンポーネントにはすべて同じコンストラクタ引数が与えられますが、それを変更するには、次...の行に a を追加するだけです// Note: ...

#include <tuple>
#include <type_traits>

template<typename...T> struct ConstructTuple {
   // For convenience, the resulting tuple type
   using type = std::tuple<T...>;
   // And the tuple of references type
   using ref_type = std::tuple<T&...>;

   // Wrap each component in a struct which will be used to construct the component
   // and hold its value.
   template<typename U> struct Wrapper {
      U value;
      template<typename Arg>
      Wrapper(Arg&& arg)
          : value(std::forward<Arg>(arg)) {
      }
   };

   // The implementation class derives from all of the Wrappers.
   // C++ guarantees that base classes are constructed in order, and
   // Wrappers are listed in the specified order because parameter packs don't
   // reorder.
   struct Impl : Wrapper<T>... {
      template<typename Arg> Impl(Arg&& arg)        // Note ...Arg, ...arg
          : Wrapper<T>(std::forward<Arg>(arg))... {}
   };

   template<typename Arg> ConstructTuple(Arg&& arg) // Note ...Arg, ...arg
       : impl(std::forward<Arg>(arg)),              // Note ...
         value((static_cast<Wrapper<T>&>(impl)).value...) {
   }
   operator type() const { return value; }
   ref_type operator()() const { return value; }

   Impl impl;
   ref_type value;
};

// Finally, a convenience alias in case we want to give `ConstructTuple`
// a tuple type instead of a list of types:
template<typename Tuple> struct ConstructFromTupleHelper;
template<typename...T> struct ConstructFromTupleHelper<std::tuple<T...>> {
  using type = ConstructTuple<T...>;
};
template<typename Tuple>
using ConstructFromTuple = typename ConstructFromTupleHelper<Tuple>::type;

スピンしてみましょう

#include <iostream>

// Three classes with constructors
struct Hello { char n;   Hello(decltype(n) n) : n(n) { std::cout << "Hello, "; }; };
struct World { double n; World(decltype(n) n) : n(n) { std::cout << "world";   }; };
struct Bang  { int n;    Bang(decltype(n)  n) : n(n) { std::cout << "!\n";     }; };
std::ostream& operator<<(std::ostream& out, const Hello& g) { return out << g.n; }
std::ostream& operator<<(std::ostream& out, const World& g) { return out << g.n; }
std::ostream& operator<<(std::ostream& out, const Bang&  g) { return out << g.n; }

using std::get;
using Greeting = std::tuple<Hello, World, Bang>;
std::ostream& operator<<(std::ostream& out, const Greeting &n) {
   return out << get<0>(n) << ' ' << get<1>(n) << ' ' << get<2>(n);
}

int main() {
    // Constructors run in order
    Greeting greet = ConstructFromTuple<Greeting>(33.14159);
    // Now show the result
    std::cout << greet << std::endl;
    return 0;
}

liveworkspaceで動作を確認してください。clang と gcc の両方で同じ順序で構築されることを確認します (libc++ のタプル実装は、stdlibc++ とは逆の順序でタプル コンポーネントを保持するため、妥当なテストだと思います)。

同じコンポーネントを複数持つ可能性のあるタプルでこれを機能させるには、Wrapperコンポーネントごとに一意の構造体になるように変更する必要があります。これを行う最も簡単な方法は、シーケンシャル インデックスである 2 番目のテンプレート パラメーターを追加することです (libc++ と libstdc++ の両方がタプルの実装でこれを行います。これは標準的な手法です)。これを行うために「インデックス」の実装を起動すると便利ですが、説明のために、簡単な再帰を行っただけです。

#include <tuple>
#include <type_traits>

template<typename T, int I> struct Item {
  using type = T;
  static const int value = I;
};

template<typename...TI> struct ConstructTupleI;
template<typename...T, int...I> struct ConstructTupleI<Item<T, I>...> {
   using type = std::tuple<T...>;
   using ref_type = std::tuple<T&...>;

   // I is just to distinguish different wrappers from each other
   template<typename U, int J> struct Wrapper {
      U value;
      template<typename Arg>
      Wrapper(Arg&& arg)
          : value(std::forward<Arg>(arg)) {
      }
   };

   struct Impl : Wrapper<T, I>... {
      template<typename Arg> Impl(Arg&& arg)
          : Wrapper<T, I>(std::forward<Arg>(arg))... {}
   };

   template<typename Arg> ConstructTupleI(Arg&& arg)
       : impl(std::forward<Arg>(arg)),
         value((static_cast<Wrapper<T, I>&>(impl)).value...) {
   }
   operator type() const { return value; }
   ref_type operator()() const { return value; }

   Impl impl;
   ref_type value;
};

template<typename...T> struct List{};
template<typename L, typename...T> struct WrapNum;
template<typename...TI> struct WrapNum<List<TI...>> {
  using type = ConstructTupleI<TI...>;
};
template<typename...TI, typename T, typename...Rest>
struct WrapNum<List<TI...>, T, Rest...>
    : WrapNum<List<TI..., Item<T, sizeof...(TI)>>, Rest...> {
};

// Use WrapNum to make ConstructTupleI from ConstructTuple
template<typename...T> using ConstructTuple = typename WrapNum<List<>, T...>::type;

// Finally, a convenience alias in case we want to give `ConstructTuple`
// a tuple type instead of a list of types:
template<typename Tuple> struct ConstructFromTupleHelper;
template<typename...T> struct ConstructFromTupleHelper<std::tuple<T...>> {
  using type = ConstructTuple<T...>;
};
template<typename Tuple>
using ConstructFromTuple = typename ConstructFromTupleHelper<Tuple>::type;

ここでテストを行います。

于 2012-12-27T16:41:03.733 に答える
-1

定義を手動で展開する唯一の方法だと思います。次のようなものがうまくいくかもしれません。ただし、それをより良くする試みは歓迎します。

#include <iostream>
#include <tuple>

struct A { A(std::istream& is) {}};
struct B { B(std::istream& is) {}};

template <typename... Ts> 
class Parser
{ };

template <typename T>
class Parser<T>
{
public:
   static std::tuple<T> parse(std::istream& is) {return std::make_tuple(T(is)); }
};

template <typename T, typename... Ts>
class Parser<T, Ts...>
{
public:
   static std::tuple<T,Ts...> parse(std::istream& is) 
   {
      A t(is);
      return std::tuple_cat(std::tuple<T>(std::move(t)),
         Parser<Ts...>::parse(is));
   }
};


int main()
{
   Parser<A,B>::parse(std::cin);
   return 1;
}
于 2012-12-27T16:25:32.873 に答える