C++11 pattern for factory function returning tuple

2019-02-04 20:42发布

问题:

In my project I have some functions like

std::tuple<VAO, Mesh, ShaderProgram> LoadWavefront(std::string filename);

That I can use like this:

VAO teapotVAO;
Mesh teapotMesh;
ShaderProgram teapotShader;
std::tie(teapotVAO, teapotMesh, teapotShader)
    = LoadWavefront("assets/teapot.obj");

The problem is, this requires each of those classes to have a default constructor that creates them in an invalid state, which is error prone. How do I get around that without having to std::get<> each item? Is there an elegant way to do this?

回答1:

How do I get around that without having to std::get<> each item? Is there an elegant way to do this?

Return by value, instead of returning by "values" (which is what this std::tuple allows you to do).

API changes:

class Wavefront
{
public:
    Wavefront(VAO v, Mesh m, ShaderProgram sp); // use whatever construction
                                                // suits you here; you will
                                                // only use it internally
                                                // in the load function, anyway
    const VAO& vao() const;
    const Mesh& mesh() const;
    const ShaderProgram& shader() const;
};

Wavefront LoadWavefront(std::string filename);


回答2:

There is an inverted-control flow style that could be useful.

LoadWavefront("assets/teapot.obj", [&]( VAO teapotVAO, Mesh teapotMesh, ShaderProgram teapotShader ){
  // code
});

with VAO& reference-style instead optional. In this case, the return value of the lambda could be used as the return value of the LoadWavefront, with a default lambda that just forwards all 3 arguments out allowing "old style" access if you want. If you only want one, or want to do some stuff after it is loaded, you can also do that.

Now, LoadWavefront should probably return a future as it is an IO function. In this case, a future of tuple. We can make the above pattern a bit more generic:

template<class... Ts, class F>
auto unpack( std::tuple<Ts...>&& tup, F&& f ); // magic

and do

unpack( LoadWavefront("assets/teapot.obj"), [&]( VAO teapotVAO, Mesh teapotMesh, ShaderProgram teapotShader ){
  // code
});

unpack can also be taught about std::futures and automatically create a future of the result.

This can lead to some annoying levels of brackets. We could steal a page from functional programming if we want to be insane:

LoadWavefront("assets/teapot.obj")
*sync_next* [&]( VAO teapotVAO, Mesh teapotMesh, ShaderProgram teapotShader ){
  // code
};

where LoadWavefront returns a std::future<std::tuple>. The named operator *sync_next* takes a std::future on the left hand side and a lambda on the right hand side, negotiates a calling convention (first trying to flatten tuples), and continues the future as a deferred call. (note that on windows, the std::future that async returns fails to .wait() on destruction, in violation of the standard).

This is, however, an insane approach. There may be more code like this coming down the type with the proposed await, but it will provide much cleaner syntax to handle it.


Anyhow, here is a complete implementation of an infix *then* named operator, just because live example

#include <utility>
#include <tuple>
#include <iostream>
#include <future>

// a better std::result_of:
template<class Sig,class=void>
struct invoke_result {};
template<class F, class... Args>
struct invoke_result<F(Args...), decltype(void(std::declval<F>()(std::declval<Args>()...)))>
{
  using type = decltype(std::declval<F>()(std::declval<Args>()...));
};
template<class Sig>
using invoke_t = typename invoke_result<Sig>::type;

// complete named operator library in about a dozen lines of code:
namespace named_operator {
  template<class D>struct make_operator{};

  template<class T, class O> struct half_apply { T&& lhs; };

  template<class Lhs, class Op>
  half_apply<Lhs, Op> operator*( Lhs&& lhs, make_operator<Op> ) {
      return {std::forward<Lhs>(lhs)};
  }

  template<class Lhs, class Op, class Rhs>
  auto operator*( half_apply<Lhs, Op>&& lhs, Rhs&& rhs )
  -> decltype( invoke( std::forward<Lhs>(lhs.lhs), Op{}, std::forward<Rhs>(rhs) ) )
  {
      return invoke( std::forward<Lhs>(lhs.lhs), Op{}, std::forward<Rhs>(rhs) );
  }
}

// create a named operator then:
static struct then_t:named_operator::make_operator<then_t> {} then;

namespace details {
  template<size_t...Is, class Tup, class F>
  auto invoke_helper( std::index_sequence<Is...>, Tup&& tup, F&& f )
  -> invoke_t<F(typename std::tuple_element<Is,Tup>::type...)>
  {
      return std::forward<F>(f)( std::get<Is>(std::forward<Tup>(tup))... );
  }
}

// first overload of A *then* B handles tuple and tuple-like return values:
template<class Tup, class F>
auto invoke( Tup&& tup, then_t, F&& f )
-> decltype( details::invoke_helper( std::make_index_sequence< std::tuple_size<std::decay_t<Tup>>{} >{}, std::forward<Tup>(tup), std::forward<F>(f) ) )
{
  return details::invoke_helper( std::make_index_sequence< std::tuple_size<std::decay_t<Tup>>{} >{}, std::forward<Tup>(tup), std::forward<F>(f) );
}

// second overload of A *then* B
// only applies if above does not:
template<class T, class F>
auto invoke( T&& t, then_t, F&& f, ... )
-> invoke_t< F(T) >
{
  return std::forward<F>(f)(std::forward<T>(t));
}
// support for std::future *then* lambda, optional really.
// note it is defined recursively, so a std::future< std::tuple >
// will auto-unpack into a multi-argument lambda:
template<class X, class F>
auto invoke( std::future<X> x, then_t, F&& f )
-> std::future< decltype( std::move(x).get() *then* std::declval<F>() ) >
{
  return std::async( std::launch::async,
    [x = std::move(x), f = std::forward<F>(f)]() mutable {
      return std::move(x).get() *then* std::move(f);
    }
  );
}

int main()
{
  7
  *then* [](int x){ std::cout << x << "\n"; };

  std::make_tuple( 3, 2 )
  *then* [](int x, int y){ std::cout << x << "," << y << "\n"; };

  std::future<void> later =
    std::async( std::launch::async, []{ return 42; } )
    *then* [](int x){ return x/2; }
    *then* [](int x){ std::cout << x << "\n"; };
  later.wait();
}

this will let you do the following:

LoadWaveFront("assets/teapot.obj")
*then* [&]( VAO teapotVAO, Mesh teapotMesh, ShaderProgram teapotShader ){
  // code
}

which I find cute.



回答3:

You could use boost::optional:

boost::optional<VAO> teapotVAO;
boost::optional<Mesh> teapotMesh;
boost::optional<ShaderProgram> teapotShader;
std::tie(teapotVAO, teapotMesh, teapotShader)
    = LoadWavefront("assets/teapot.obj");

Of course you'd have to change the way you access these values to always do *teapotVAO, but at least the compiler will let you know if you mess up any of the access.



回答4:

Lets go even further, and assume there is no default constructor for those classes.

One option is something like this:

auto tup = LoadWavefront("assets/teapot.obj");
VAO teapotVAO(std::move(std::get<0>(tup)));
Mesh teapotMesh(std::move(std::get<1>(tup)));
ShaderProgram teapotShader(std::move(std::get<2>(tup)));

This still leaves around the tup as a mostly cleaned up opject, which is less than ideal.

But wait...why does those even need to have ownership?

auto tup = LoadWavefront("assets/teapot.obj");
VAO& teapotVAO=std::get<0>(tup);
Mesh& teapotMesh=std::get<1>(tup);
ShaderProgram& teapotShader=std::get<2>(tup);

As long as the references are in the same scope as the returned tuple, there is no problem here.

Personally, this seems like a clear place where one should use a set of smart pointers instead of this nonsense:

LoadWavefront(const char*,std::unique_ptr<VAO>&,std::unique_ptr<Mesh>&,std::unique_ptr<ShaderProgram>&);

std::unique_ptr<VAO> teapotVAO;
std::unique_ptr<Mesh> teapotMesh;
std::unique_ptr<ShaderProgram> teapotShader;
LoadWavefront("assets/teapot.obj",teapotVAO,teapotMesh,teapotShader);

This will take care of the ownership issue and allow a sensible null state.

Edit: /u/dyp pointed that you could use the following with the original output style

std::unique_ptr<VAO> teapotVAO;
std::unique_ptr<Mesh> teapotMesh;
std::unique_ptr<ShaderProgram> teapotShader;
std::tie(teapotVAO,teapotMesh,teapotShader) = LoadWavefront("assets/teapot.obj");