Подтвердить что ты не робот

Как избежать undefined порядка выполнения для конструкторов при использовании std:: make_tuple

Как я могу использовать std:: make_tuple, если порядок выполнения конструкторов важен?

Например, я предполагаю, что порядок выполнения конструктора класса A и конструктора класса B равен undefined для:

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

реализация имеет порядок выполнения undefined конструкторов.

Обновить, предоставляя некоторый контекст:

Чтобы дать дополнительную информацию о том, что я пытаюсь сделать, вот эскиз:

Я хочу прочитать некоторые сериализованные объекты из stdin с помощью CodeSynthesis XSD двоичный синтаксический анализ/сериализация, Вот пример того, как выполняется такой синтаксический анализ и сериализация: example/cxx/tree/binary/xdr/driver.cxx

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

Я хочу иметь возможность указать список классов, которые имеют сериализованные объекты (например, каталог, каталог, someOtherSerializableClass для 3 сериализованных объектов) и хранить эту информацию в виде 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>
4b9b3361

Ответ 1

Тривиальное решение состоит не в том, чтобы использовать 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], в котором говорится, что она соответствует правилам семантики инициализации списка (обратите внимание: это не имеет никакого отношения к std:: initializer_list, который работает только с однородными типами). Ограничение порядка находится в пункте 8.5.4 [dcl.init.list].

Ответ 2

Как говорится в комментарии, вы можете просто использовать список инициализаторов:

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, i.e args = {X, Y, Z}, и тогда мы можем думать по этим строкам, шаг за шагом продвигаясь к решению:

  • В первую очередь, конструкторы X, Y и Z могут выполняться в любом порядке, поскольку порядок, в котором вычисляются аргументы функции, не определен стандартом С++.

  • Но мы хотим сначала построить X, затем Y и Z. Или, по крайней мере, мы хотим имитировать это поведение, что означает, что X должен быть сконструирован с данными, которые находятся в начале входного потока (скажем, что данные xData) и Y должны быть построены с данными, которые поступают немедленно после 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, который интересно. Кроме того, используемый здесь метод может быть полезен для мест, где нельзя использовать инициализацию списка.

Ответ 3

Здесь нет ничего особенного в make_tuple. Любой вызов функции в С++ позволяет его аргументы вызываться в неуказанном порядке (позволяя свободу компилятора оптимизировать).

Я действительно не предлагаю конструкторов, которые имеют побочные эффекты, такие, что порядок важен (это будет кошмар обслуживания), но если вам это абсолютно необходимо, вы всегда можете построить объекты явно, чтобы установить порядок, хотите:

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

Ответ 4

Этот ответ исходит из комментария, который я сделал для вопроса о пакете шаблонов

Так как make_tuple выводит тип кортежа из построенных компонентов, а аргументы функции имеют конструктор оценки undefined, конструкция должна произойти внутри машины, что я и предложил в комментарии. В этом случае нет необходимости использовать 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 (реализация libС++ tuple содержит компоненты кортежа в обратном порядке к stdlibС++, поэтому я думаю, что это разумный тест.)

Чтобы сделать эту работу с кортежами, которые могут иметь более одного компонента, необходимо изменить Wrapper как уникальную структуру для каждого компонента. Самый простой способ сделать это - добавить второй параметр шаблона, который является последовательным индексом (как libС++, так и libstdС++ делают это в своих реализациях кортежей, это стандартный метод). Было бы удобно, чтобы реализация "индексов" начиналась, чтобы сделать это, но для целей изложения я только что сделал быстро-грязную рекурсию:

#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;

С тестом здесь.

Ответ 5

Я считаю, что единственный способ вручную развернуть определение. Может случиться что-то вроде следующего. Я приветствую попытки сделать его приятнее, хотя.

#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;
}