使用std :: make_tuple时如何避免构造函数的未定义执行顺序

时间:2012-12-27 14:10:15

标签: c++ c++11 tuples variadic-templates

如果构造函数的执行顺序很重要,我如何使用std :: make_tuple?

例如,我猜A类的构造函数和类B的构造函数的执行顺序未定义为:

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

我在阅读了对该问题的评论后得出了这个结论

Translating a std::tuple into a template parameter pack

说这个

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;

Is it possible to “store” a template parameter pack without expanding it?

中所述

并找到一种方法来在解析完成后让std :: tuple使用。草图:

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

其中serializedObjects的类型为

std::tuple<catalog, catalog, someOtherSerializableClass>

5 个答案:

答案 0 :(得分:9)

简单的解决方案首先使用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段中。

答案 1 :(得分:3)

正如评论所说,你可以使用initializer-list:

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

适用于std::tuple和solikes(支持初始化列表)。

但我得到了另一种更通用的解决方案,并且在无法使用初始化列表的情况下非常有用。所以我们不使用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},然后我们可以按照这些思路一步一步地向解决方案迈进:

  • 首先,XYZ的构造函数可以按任何顺序执行,因为函数参数的计算顺序是 C ++标准的未指定

  • 但我们希望首先构建X,然后YZ。或者至少我们想要模拟该行为,这意味着X必须使用位于输入流开头的数据构建(例如 数据为xData)并且Y必须使用 xData后立即的数据构建,依此类推。

  • 众所周知,X不能保证首先构建,所以我们需要假装。基本上,我们将从流读取数据,就好像它在流的开头,即使首先构造Z,这似乎是不可能的。只要我们从输入流读取数据就不可能,但是我们从某些可索引数据结构中读取数据,例如std::vector,那么它是可能的。< / p>

  • 所以我的解决方案是这样做的:首先会填充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

这是预料之中的。请亲自查看demo at ideone。 : - )

请注意,此解决方案通过读取第一次调用read_arg本身中的所有行来避免读取流的顺序问题,并且所有后来的调用只是从{{1使用索引。

现在你可以在类的构造函数中放入一些printf,只是为了看到构造的顺序std::vector函数模板的模板参数的顺序相同,这很有趣。此外,此处使用的技术对于无法使用列表初始化的位置非常有用。

答案 2 :(得分:2)

这里make_tuple没有什么特别之处。 C ++中的任何函数调用都允许以未指定的顺序调用其参数(允许编译器自由优化)。

我真的不建议使用具有副作用的构造函数,以便顺序很重要(这将是一个维护噩梦),但是如果你绝对需要这个,你总是可以明确地构造对象来设置你的顺序想:

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

答案 3 :(得分:0)

这个答案来自我对the template pack question

的评论

由于make_tuple从构造的组件中推断出元组类型,并且函数参数具有未定义的评估ordder,所以构造必须在机器内部发生,这是我在评论中提出的。在这种情况下,不需要使用make_tuple;你可以直接从元组类型构造元组。但这也不是订购建设;我在这里做的是构造元组的每个组件,然后构建一个对组件的引用元组。如果组件易于移动或复制,则可以轻松地将引用元组转换为所需类型的元组。

这里的解决方案(来自评论中的lws链接)略有修改,并进行了一些解释。这个版本只处理类型各不相同的元组,但它更容易理解;还有另一个版本,它正确。与原始元素一样,元组组件都被赋予相同的构造函数参数,但更改它只需要在...

指示的行中添加// 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修改为每个组件的唯一结构。最简单的方法是添加第二个模板参数,这是一个顺序索引(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;

使用测试here

答案 4 :(得分:-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;
}