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?
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::future
s 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 tuple
s), 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.
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);
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.
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");
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With