【问题标题】:C++11 pattern for factory function returning tuple工厂函数返回元组的 C++11 模式
【发布时间】:2014-11-21 18:58:54
【问题描述】:

在我的项目中,我有一些功能,例如

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

我可以这样使用:

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

问题是,这要求这些类中的每一个都有一个默认构造函数,该构造函数将它们创建为无效状态,这很容易出错。我如何解决这个问题而不必std::get&lt;&gt; 每个项目?有没有优雅的方法来做到这一点?

【问题讨论】:

  • 我认为您的意思是“有一个默认构造函数在有效状态中创建它们”?至于std::get&lt;&gt;,您是要完全避免这种情况,还是可以尽量减少使用次数?
  • 我的建议:不要使用tuple,而是使用自定义类型。看来您不是在进行通用编程,因此没有理由使用不为其成员指定体面名称的类型。
  • @hvd 不幸的是,有时这些类型没有合理的默认构造函数。
  • @dyp 这实际上很有意义。然后我有一个构造函数,而不是一个工厂函数,而不是teapotMesh,我有teapot.Mesh,并且所有漂亮的清理/引用位置都是免费的。

标签: c++ c++11 factory


【解决方案1】:

有一种反向控制流样式可能很有用。

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

使用VAO&amp; 引用样式而不是可选的。在这种情况下,lambda 的返回值可以用作LoadWavefront 的返回值,默认的 lambda 只转发所有 3 个参数,如果您愿意,可以允许“旧式”访问。如果你只想要一个,或者想在加载后做一些事情,你也可以这样做。

现在,LoadWavefront 可能应该返回一个future,因为它是一个 IO 函数。在这种情况下,futuretuple。我们可以让上面的模式更通用一点:

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

然后做

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

unpack 也可以被教授std::futures 并自动创建结果的未来。

这可能会导致一些烦人的括号级别。如果我们想发疯,我们可以从函数式编程中窃取页面:

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

其中LoadWavefront 返回std::future&lt;std::tuple&gt;。命名运算符 *sync_next* 在左侧接受 std::future,在右侧接受 lambda,协商调用约定(首先尝试压平 tuples),然后将 future 作为延迟调用继续. (注意在windows上,async返回的std::future在销毁时失败.wait(),违反标准)。

然而,这是一种疯狂的方法。可能会有更多这样的代码出现在建议的 await 类型中,但它会提供更简洁的语法来处理它。


总之,这是一个中缀*then*命名操作符的完整实现,因为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();
}

这将让您执行以下操作:

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

我觉得很可爱。

【讨论】:

  • 老问题的有趣解决方案!
  • 同意,这种功能技术太疯狂了:)
  • 我真的很喜欢使用future 的想法,它很自然地将(可能是一个队列)阻塞磁盘操作放在另一个线程中。
  • 我从没想过我会看到有人在 C++ 中使用 CPS。这很聪明。不过,没有必要将std::future 烘焙成LoadWaveFront;呼叫总是可以被包装的。
  • @dyp 我差了一个。 std::future *then* 支持需要 306 个字符,上面的评论是 307。供我猜测。
【解决方案2】:

如何在不必 std::get 每个项目的情况下解决这个问题?有没有优雅的方法来做到这一点?

按值返回,而不是按“值”返回(这是 std::tuple 允许您执行的操作)。

API 更改:

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

【讨论】:

    【解决方案3】:

    你可以使用boost::optional:

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

    当然,您必须将访问这些值的方式更改为始终执行*teapotVAO,但至少编译器会在您搞砸任何访问时通知您。

    【讨论】:

      【解决方案4】:

      让我们更进一步,假设这些类没有默认构造函数。

      一个选项是这样的:

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

      这仍然作为一个大部分清理过的对象留在 tup 周围,这不太理想。

      但是等等……为什么那些甚至需要拥有所有权?

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

      只要引用和返回的元组在同一范围内,这里就没有问题。

      就个人而言,这似乎是一个明确的地方,应该使用一组智能指针而不是这种废话:

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

      这将解决所有权问题并允许合理的空状态。

      编辑:/u/dyp 指出您可以将以下内容与原始输出样式一起使用

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

      【讨论】:

      • 您可以将 OP 的样式与unique_ptrs 一起使用,而不是使用输出参数,并将它们以元组形式返回。
      • @dyp 哦,是的,rhr 会允许这样做的。将编辑
      • @dyp hrm...任何关于这是否可能是 RVO 的知识?
      • 我们看不到 RVO 是否在此代码中发生,因为没有 return ;) 作为返回值 LoadWavefront 的(可能)临时创建不能被省略,因为这一行确实不要从它构造一个对象(它只是分配给一个元组)。但是我们这里不需要 RVO,这行所做的只是复制三个指针,然后将源归零。
      • @dyp 完全同意这里不需要它...如果不是移动,unique_ptr 不会让它发生,所以最坏的情况是你将有 6 个指针副本(3 到返回,3 到值)。 RVO 在原始问题的情况下会更有用(也许?)但谁知道呢。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-11-30
      • 1970-01-01
      • 1970-01-01
      • 2016-10-09
      • 1970-01-01
      • 1970-01-01
      • 2016-03-26
      相关资源
      最近更新 更多