【问题标题】:Handling void assignment in C++ generic programming在 C++ 泛型编程中处理 void 赋值
【发布时间】:2018-06-08 08:33:33
【问题描述】:

我有包装任意 lambda 并返回 lambda 结果的 C++ 代码。

template <typename F>
auto wrapAndRun(F fn) -> decltype(F()) {
    // foo();
    auto result = fn();
    // bar();
    return result;
}

除非F 返回void (error: variable has incomplete type 'void'),否则此方法有效。我曾想过使用ScopeGuard 运行bar,但如果fn 抛出,我不希望bar 运行。有什么想法吗?

附:后来发现有a proposal to fix this inconsistency

【问题讨论】:

  • 哪个版本的 C++,14 还是 17?
  • 这是个人项目,所以c++2x也可以。
  • 之前遇到过同样的问题,同样的解决方案适用。 stackoverflow.com/questions/24468397/… 虽然只是 C++11,但可能会被现代化。
  • 你看过std::invoke吗?看起来正是您需要的
  • @SemyonBurov std::invoke 将如何帮助我?

标签: c++


【解决方案1】:

这是一个非常尴尬且不可扩展的解决方案,但非常简短且简单,它支持RVO,这对于某些返回类型可能很重要:

template <typename F>
auto wrapAndRun(F fn) -> decltype(F()) {
    // foo();
    char c;
    auto runner = std::unique_ptr<char, decltype(bar)>( &c, bar );
    try {
        return fn();
    }
    catch( ... ) {
        runner.release();
        throw;
    }
}

【讨论】:

  • 对于 RVO:我误读了代码的关键部分。 RVO 应该可以正常工作。
  • 什么是 RVO...?
  • @PeterMortensen,返回值优化。
【解决方案2】:

您可以编写一个简单的包装类来处理这部分内容:

template <class T>
struct CallAndStore {
    template <class F>
    CallAndStore(F f) : t(f()) {}
    T t;
    T get() { return std::forward<T>(t); }
};

并且专精:

template <>
struct CallAndStore<void> {
    template <class F>
    CallAndStore(F f) { f(); }
    void get() {}
};

你可以通过一个小的工厂函数来提高可用性:

template <typename F>
auto makeCallAndStore(F&& f) -> CallAndStore<decltype(std::declval<F>()())> {
    return {std::forward<F>(f)};
}

那就用吧。

template <typename F>
auto wrapAndRun(F fn) {
    // foo();
    auto&& result = makeCallAndStore(std::move(fn));
    // bar();
    return result.get();
}

编辑:在get 中使用std::forward 转换,这似乎也可以正确处理从函数返回的引用。

【讨论】:

  • 我更喜欢您的解决方案,但是基于返回类型必须存储在您的案例中的事实是否存在一些限制?
  • 抱歉,具体的限制是什么?它只需要是可移动的(让我添加一个 std::move)。
  • 也许不是限制而是差异,例如我不确定在您的情况下可以避免 T 的析构函数,因为无法应用 RVO
  • @Curious 好把戏,我同意这绝对更好。随意编辑答案以包括您的改进(并删除我对扣除指南的评论)。
  • @Curious 我在回答中添加了您的改进,谢谢!
【解决方案3】:

使用 SFINAE 的解决方案,其想法是在内部创建一个 void 函数实际上返回 int - 希望编译器会优化它。在外部wrapAndRun 将返回与包装函数相同的类型。

http://coliru.stacked-crooked.com/a/e84ff8f74b3b6051

#include <iostream>

template <typename F>
auto wrapAndRun1(F fn) -> std::enable_if_t<!std::is_same_v<std::result_of_t<F()>, void>, std::result_of_t<F()>> {
    return fn();
}


template <typename F>
auto wrapAndRun1(F fn) -> std::enable_if_t<std::is_same_v<std::result_of_t<F()>, void>, int> {
    fn();
    return 0;
}

template <typename F>
auto wrapAndRun(F fn) -> std::result_of_t<F()> {
    // foo();
    [[maybe_unused]] auto result = wrapAndRun1(fn);    
    // bar();        
    if constexpr (std::is_void_v<std::result_of_t<F()>>)
        return;
    else
        return result;
}

int main()
{
    wrapAndRun([] { std::cout << "with return \n"; return 0; });
    wrapAndRun([] { std::cout << "no return \n"; });
}

【讨论】:

  • 我认为 OP 想要运行 bar() 以防 fn() 返回类型为 void
  • 不确定为什么顶部 enable_if 似乎有一些重复?我认为无论如何编写一个单独的returns_void 特征都会使答案受益。不过,这里的显着缺点是您必须在两个地方重复 foobar(实际上已经忘记了一个)。
  • 代码重复很难看,如果可以避免的话,我认为这不是一个好的解决方案。
  • @Nir Friedman - 我已删除重复项。我觉得它现在看起来很不错。
  • @marcinj 很抱歉成为一个书呆子,但我有点担心当传递的函数返回 void 时,包装器仍然返回 0。这对于 OP 来说可能“足够好”,但我觉得它不是最优的;可以防止在编译时发现错误。
【解决方案4】:

新增的 C++17 if constexpr 在这里可能会有所帮助。你可以在编译时选择是否返回fn()的结果:

#include <type_traits>

template <typename F>
auto wrapAndRun(F fn) -> decltype(fn())
{
    if constexpr (std::is_same_v<decltype(fn()), void>)
    {
        foo();
        fn();
        bar();
    }
    else
    {
        foo();
        auto result = fn();
        bar();
        return result;
    }
}

正如你所说的 C++2a 也是一个选项,你也可以利用概念,对函数施加约束:

template <typename F>
  requires requires (F fn) { { fn() } -> void }
void wrapAndRun(F fn)
{
    foo();
    fn();
    bar();
}

template <typename F>
decltype(auto) wrapAndRun(F fn)
{
    foo();
    auto result = fn();
    bar();
    return result;
}

【讨论】:

  • 可以在不重复调用foobar 的情况下做到这一点吗?
  • @NirFriedman 是的,但我认为这种权衡并不好。毕竟,这段代码不会因为重复而痛苦,而且足够简单。
  • 抱歉,具体的取舍是什么?此外,假设实际上有两个函数调用。在 OP 的情况下或一般情况下,那里可能有更多代码,用户可能会或可能不会觉得将其分解为函数。
  • @NirFriedman:如果那里有更多代码,他可能应该将其重构为foo()bar()
  • @MooingDuck 错误,为什么?你必须有代码的地方,你知道,它不能都是函数调用。这样的保理可能是正确的选择,甚至在大多数情况下,但也可能不是所有时间。基本上迫使您考虑foobar 的解决方案比不考虑的解决方案更糟糕(当然,所有其他条件都相同)。
【解决方案5】:

另一个技巧可能是利用逗号运算符,例如:

struct or_void {};

template<typename T>
T&& operator,( T&& x, or_void ){ return std::forward<T>(x); }

template <typename F>
auto wrapAndRun(F fn) -> decltype(fn()) {
    // foo();
    auto result = ( fn(), or_void() );
    // bar();
    return decltype(fn())(result);
}

【讨论】:

  • @MassimilianoJanes 重载逗号运算符是大部分 C++ 社区(包括自己)会告诉你永远不要做的事情之一。是的,我理解这个想法,但这是 100% 的,死心塌地,那种黑魔法超载,所以,很多人和风格指南都建议反对。如果真的、真的没有其他方法可能值得考虑,但这里已经有 4 个解决方案都更可取。
  • @NirFriedman 尊敬的,我不同意;首先,逗号运算符 is 在现有的广泛使用的库中使用(提升,Eigen 引用我想到的第一个),因此声称 每个人 说从不 使用它,简直是假的。其次,人们应该(正确地)避免重载逗号运算符的原因在这里不适用,您可以轻松地做到这一点。
  • 第三,我们不知道OP代码的最终目的和范围;他可能只需要一个问题的技术解决方案,隐藏在实施细节中。这个答案是 OP 问题的有效解决方案。 Afaik,仅仅因为它对你“感觉”不好就投反对票,这违背了投票的意义。
  • 除此之外,它也不能正确处理返回引用类型的函数(很容易修复),并且它还会使 RVO 双重无效,即当我运行此代码时,有 2 个无关的析构函数调用,相反在此处的大多数其他解决方案中找到的 1 或 0。
  • @NirFriedman 嗯,您在评论中链接的答案提到逗号可以用作元编程技术(有时确实如此)以及其他合法用途。坦率地说,我相信存在(或应该存在)指导方针,作为传达某些事情不好的原因的一种手段,而不是出售关于一个人应该总是/永远不要做的教条。关于否决票问题,我认为投票机制已经允许建立答案的层次结构;如果我正确阅读了 SO 帮助和元帖子,则应将反对票保留给无用或错误的答案。
【解决方案6】:

你可以看看 Alexandrescu 的 ScopeGuard:ScopeGuard.h 它只在没有异常时执行代码。

template<class Fn>
decltype(auto) wrapAndRun(Fn&& f) {
  foo();
  SCOPE_SUCCESS{ bar(); }; //Only executed at scope exit when there are no exceptions.
  return std::forward<Fn>(f)();
}

所以在没有错误的情况下,执行顺序是:1. foo(), 2. f(), 3. bar()。如果出现异常,顺序为:1. foo(), 2. f()

【讨论】:

  • 这很有趣。我没有意识到在用户空间中可以在 17 之前写 uncaught_exceptions(注意 s);这是 SCOPE_SUCCESS 之类的先决条件。不错的答案!
猜你喜欢
  • 1970-01-01
  • 2011-01-28
  • 1970-01-01
  • 1970-01-01
  • 2011-08-01
  • 2014-05-24
  • 2010-12-30
  • 2014-02-09
  • 1970-01-01
相关资源
最近更新 更多