【问题标题】:Handling a void variable in a templatized function in C++11在 C++11 的模板化函数中处理 void 变量
【发布时间】:2017-03-10 18:31:45
【问题描述】:

我有一个模板类,它必须在调用参数和返回类型都是泛型的函数之前执行一些操作。

这是方法:

template <typename ReturnType, typename ...Args>
ReturnType function (Args ...args) {
  // prepare for call
  // ...
  ReturnType rv = makeCall(args...);  // [1]
  // dismiss the call
  // ...
  return rv;
}

ReturnType 不是void 时,它当然可以正确编译。 当我在这种情况下使用它时:

function<void>(firstArg, secondArg);

编译器响应

error: return-statement with a value, in function returning 'void' [-fpermissive]

指向标有 [1] 的行。

除了将-fpermissive 传递给编译器之外,还有其他解决方案吗? 我希望有一个独特的方法,因为我发现可能的解决方案是使用enable_ifis_same 实例化不同的版本。

提前谢谢你。

-- 更新--

这是一个完整的例子。我应该说我们的函数确实是类方法。

#include <type_traits>
#include <iostream>

class Caller {
public:
    Caller() {}

    template <typename ReturnType, typename ...Arguments>
    ReturnType call(Arguments ... args) {
        prepare();

        ReturnType rv = callImpl<ReturnType>(args...);

        done();

        return rv;
    }

private:
    void prepare() {
        std::cout << "Prepare\n";
    }

    void done() {
        std::cout << "Done\n";
    }

    template <typename ReturnType, typename ...Arguments>
    typename std::enable_if<std::is_same<ReturnType, void>::value, ReturnType>::type callImpl ( Arguments ... args) {
        std::cout << "Calling with void\n";
        return;
    }

    template <typename ReturnType, typename ...Arguments>
    typename std::enable_if<std::is_same<ReturnType, bool>::value, ReturnType>::type callImpl (Arguments ... args) {
        std::cout << "Calling with bool\n";
        return true;
    }

    template <typename ReturnType, typename ...Arguments>
    typename std::enable_if<std::is_same<ReturnType, int>::value, ReturnType>::type callImpl (Arguments ... args) {
        std::cout << "Calling with int\n";
        return 42;
    }
};


int main(int argc, char *argv[]) {

    Caller c;
    auto rbool = c.call<bool> (1,20);
    std::cout << "Return: " << rbool << "\n";
    auto rint = c.call<int> (1,20);
    std::cout << "Return: " << rint << "\n";

    // the next line fails compilation. compile with --std=c++11
    c.call<void>("abababa");

    return 0;
}

-- 更新--

不是什么大问题:使用std::bind(&amp;Caller::callImpl&lt;ReturnType&gt;, this, args)

【问题讨论】:

  • 你试过function&lt;void*&gt; 吗?
  • 在 C++17 中,您可能会使用 constexpr if (std::is_same&lt;ReturnType,void&gt;::value) { return; } else { return rv; }(好吧,这不是整个解决方案,因为您也不应该尝试实例化 void rv,但您会发现偏差)。
  • 谢谢@Someprogrammerdude,这是我想避免的,但目前这是最好的解决方案
  • 谢谢@einpoklum,我知道这个解决方案,但它不可行,因为我的上下文不支持 c++17。还是谢谢你。
  • 部分特化是错误的,我真的不知道我在想什么,但不可能部分特化函数。重载和使用例如enable_if 或其他一些类型特征可能是一个可能的解决方案。

标签: c++ c++11 templates variadic


【解决方案1】:

这是我对通用 C++11 兼容解决方案的尝试,您可以轻松重用该解决方案。

让我们从创建一个简单的类型特征开始,它将void 转换为一个空结构。这不会引入任何代码重复。

struct nothing { };

template <typename T>
struct void_to_nothing 
{
    using type = T;
};

template <>
struct void_to_nothing<void>
{
    using type = nothing;
};

template <typename T>
using void_to_nothing_t = typename void_to_nothing<T>::type; 

我们还需要一种调用任意函数的方法,将最终的void 返回类型转换为nothing

template <typename TReturn>
struct helper
{
    template <typename TF, typename... Ts>
    TReturn operator()(TF&& f, Ts&&... xs) const
    {
        return std::forward<TF>(f)(std::forward<Ts>(xs)...);
    }
};

template <>
struct helper<void>
{
    template <typename TF, typename... Ts>
    nothing operator()(TF&& f, Ts&&... xs) const
    {
        std::forward<TF>(f)(std::forward<Ts>(xs)...);
        return nothing{};
    }
};

template <typename TF, typename... Ts>
auto with_void_to_nothing(TF&& f, Ts&&... xs)
    -> void_to_nothing_t<
           decltype(std::forward<TF>(f)(std::forward<Ts>(xs)...))>
{
    using return_type = 
        decltype(std::forward<TF>(f)(std::forward<Ts>(xs)...));

    return helper<return_type>{}(std::forward<TF>(f), std::forward<Ts>(xs)...);
}

用法:

template <typename ReturnType, typename ...Args>
void_to_nothing_t<ReturnType> function (Args ...args) {
  // prepare for call
  // ...
  auto rv = with_void_to_nothing(makeCall, args...);  // [1]
  // dismiss the call
  // ...
  return rv;
}

live wandbox example


Matt Calabrese 提出了一个名为 “Regular Void” 的提案,可以解决这个问题。 You can find it here: "P0146R1".

【讨论】:

  • 还有一个问题是function 不再返回void,而是返回nothing。这暴露了一个内部实现细节。
  • 谢谢,尽管如评论中所述,此解决方案运行良好,但函数返回 nothing 而不是 void,这对于我的目的来说是完全可以接受的。
【解决方案2】:

取决于你希望在行中完成什么

// dismiss the call

你也许可以使用:

template <typename ReturnType, typename ...Args>
ReturnType function (Args ...args) {
  // prepare for call
  // ...

  CallDismisser c;
  return  makeCall(args...);  // [1]
}

只要CallDismisser 的析构函数可以完成你需要做的所有事情,它就可以工作。

【讨论】:

  • 这是一个可能的解决方案,我没有测试过,但它似乎是正确的。唯一的问题是流程不可读,除非您知道 CallDismisser 析构函数的副作用。不过谢谢,这是一个可行的解决方案。
【解决方案3】:
struct nothing {};


template<class Sig>
using returns_void = std::is_same< std::result_of_t<Sig>, void >;

template<class Sig>
using enable_void_wrap = std::enable_if_t< returns_void<Sig>{}, nothing >;
template<class Sig>
using disable_void_wrap = std::enable_if_t< !returns_void<Sig>{}, std::result_of_t<Sig> >;

template<class F>
auto wrapped_invoker( F&& f ) {
  return overload(
    [&](auto&&...args)->enable_void_wrap<F(decltype(args)...)> {
      std::forward<F>(f)(decltype(args)(args)...);
      return {};
    },
    [&](auto&&...args)->disable_void_wrap<F(decltype(args)...)> {
      return std::forward<F>(f)(decltype(args)(args)...);
    }
  );
}

所以wrapped_invoker 接受一个函数对象,并使其返回nothing 而不是void

接下来,holder

template<class T>
struct holder {
  T t;
  T&& get()&& { return std::forward<T>(t); }
};
template<>
struct holder<void> {
  template<class T>
  holder(T&&) {} // discard
  void get()&& {}
};

holder 让您保留返回值并在需要时转换回void。您必须使用 {} 创建 holder&lt;T&gt; 才能使引用生命周期延长正常工作。向holder&lt;T&gt; 添加一个ctor 会破坏它。

holder&lt;void&gt; 默默地丢弃任何传递给它的东西。

template <typename ReturnType, typename ...Args>
ReturnType function (Args ...args) {
  // prepare for call
  // ...
  holder<ReturnType> rv{ wrapped_invoker(makeCall)(args...) };
  // dismiss the call
  // ...
  return std::move(rv).get();
}

现在,holder&lt;ReturnType&gt; 不包含任何内容或 makeCall(args...) 的返回值。

如果它什么都没有,rv.get() 返回 void,并且将 void 返回到 ReturnValuevoid 的函数是合法的。

基本上我们在做两个技巧。首先,我们阻止makeCall 返回void,其次,如果我们返回void,我们将有条件地丢弃makeCall 的返回值。

overload 没有写在这里,但它是一个接受 1 个或多个函数对象(例如 lambdas)并返回其重载集的函数。有一个std::overload 的提案,以及关于 stackoverflow 本身的无数示例。

这里有一些:

【讨论】:

  • 我喜欢holder 的想法。我假设你强迫用户std::move持有人只是为了明确表示它不应该被重复使用,对吗?
  • @VittorioRomeo 是的,当我“转发”结果时,重复使用它通常并不安全。嗯,我现在想起来需要调整它,因为ReturnType=int 可以容纳int&amp;,但我的holder 拥有int&amp;&amp;,它拒绝容纳int。固定。
  • 这需要 c++14 吗?我的 clang 抱怨 lambda 参数中的 auto 。 (我也应该说makeCall() 是同一类的成员函数。我会用完整的源更新问题。谢谢)
  • @HappyCactus 是的,auto 返回类型是 C++14。您可以添加 -&gt;decltype( return_statement_goes_here ) 以实现 C++11 兼容性,或添加 #define RETURNS(...) decltype(__VA_ARGS__) { return __VA_ARGS__; } 然后 auto wrapped_invoker( F&amp;&amp; f )-&gt;RETURNS( return_statement_goes_here ) 以避免重复。此外,您必须将 auto&amp;&amp;... lambda 替换为函数对象(请注意,许多 C++11 编译器在支持 C++14 的其余部分之前支持 auto&amp;&amp;... lambda,因为它真的很容易编写)。
  • 谢谢,这是我的首选解决方案,只是我无法切换到 c++14。还是谢谢你。
【解决方案4】:

问题似乎出在//Dismiss the call

此代码不应该存在。这就是我们拥有 RAII 的目的。以下代码确实有效,即使使用 ReturnType = void

template <typename ReturnType, typename ...Arguments>
ReturnType call(Arguments ... args) {
    Context cx;
    return callImpl<ReturnType>(args...);
}

Context::Context()  { std::cout << "prepare\n"; }
Context::~Context() { std::cout << "done\n"; }

【讨论】:

  • 这个解决方案已经在这里发布了:stackoverflow.com/a/42725991/4000694,正如我所说的,它是可行的,虽然我不太喜欢它,因为它使代码的可读性降低。无论如何,谢谢你的提示。
  • @HappyCactus:这实际上是这个解决方案的一半:它涵盖了析构函数部分,但不包括构造函数。
猜你喜欢
  • 2012-05-13
  • 2015-07-04
  • 2018-08-19
  • 1970-01-01
  • 2012-08-14
  • 1970-01-01
  • 2021-05-12
  • 2015-08-21
相关资源
最近更新 更多