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)...);
}

implementation 具有未定义的构造函数执行顺序。

更新,提供一些上下文:

为了给我想要做的事情提供更多背景,这里有一个草图:

我想借助CodeSynthesis XSD二进制解析/序列化从标准输入读取一些序列化对象。以下是如何完成此类解析和序列化的示例: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

正如评论所说,你可以只使用初始化列表:

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

这将适用于std::tuple和类似(支持初始化列表)。

但是我得到了另一个更通用的解决方案,并且在不能使用初始化列表的情况下很有用。所以让我们在不使用初始化列表的情况下解决这个问题:

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

在我解释我的解决方案之前,我想先讨论一下这个问题。事实上,一步一步地思考问题也有助于我们最终找到解决方案。因此,为了简单的讨论(和思考过程),让我们假设args扩展为 3 种不同的类型,即。X, Y, Z, ieargs = {X, Y, Z}然后我们可以沿着这些思路思考,一步一步地找到解决方案:

  • 首先,X,Y和的构造函数Z可以按任何顺序执行,因为C++ 标准未指定函数参数的求值顺序。

  • 但是我们X要先构造,然后构造 ,然后Y构造Z。或者至少我们想模拟这种行为,这意味着X必须用输入流开头的数据(比如数据xDataY构建,并且必须用紧跟xData 之后的数据构建,依此类推。

  • 众所周知,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 链接),并进行了一些解释。这个版本只处理类型不同的元组,但更容易理解;下面还有另一个版本可以正确执行。和原来的一样,元组组件都被赋予了相同的构造函数参数,但改变它只需要...在用// 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;

在这里进行测试。

于 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 回答