【问题标题】:Functor that automatically deduces return and parameter types自动推导返回和参数类型的函子
【发布时间】:2019-01-30 20:23:02
【问题描述】:

假设您有一个经常重复但有一些变化的通用工作流程:

  • 锁定互斥体
  • 执行一些操作
  • 解锁互斥锁

我正在尝试实现一种可以自动执行任意操作的机制(在 C++98 中)。例如,以下内容:

myMutex.acquire();
int a = foo(arg1, arg2, arg3);
myMutex.release();
return a;

可能变成:

return doMutexProtected(myMutex, foo, arg1, arg2, arg3);

或者一些类似的机制。挑战在于如何对任意类型的a 以及任意类型和数量的参数执行此操作。

我觉得应该有一种方法可以使用模板来做到这一点,但我不确定如何实现它。你可以用仿函数做类似的事情,但你必须提前告诉仿函数它们的参数类型——我希望有一种方法可以从被调用的原始函数中自动检测到它们。这样,如果(何时)函数的参数列表发生变化,您无需更新任何内容,只需调用您调用的参数列表。

这可能吗?

【问题讨论】:

  • 所以你必须在 C++98/03 中这样做?
  • Parameter packs 是为此目的在 c++11 中设计和添加的。这在 c++11 或更高版本中很容易做到。早期 c++ 版本的 hack 通常让您选择任意最大数量的参数并为每个数量的参数实现类型/重载。虽然类型推导一直在 c++ 中工作(据我所知)。
  • @NathanOliver 不幸的是,我现在受限于 C++98/03。但是我很想知道如何做到这一点,即使在最近的 C++ 版本中也能做到这一点!
  • 任何现代 C++ 书籍都会有大量的阅读材料可供阅读。这是现代 C++ 最复杂的方面之一,无法在 stackoverflow.com 上的简短评论或答案中完全解释。 Stackoverflow.com 并不是一个真正的教程网站,有人去那里学习有关 C++ 或任何其他语言的新知识。可能这里唯一可以做的就是一个带有可调用对象和一个参数的模板。所有函数(可调用对象)都将单个类 ref 作为参数,并将其真实参数作为类成员。

标签: c++ templates c++03 c++98


【解决方案1】:

在现代 C++ (C++17) 中,函数看起来像

template <typename Mutex, typename Func, typename... Args>
decltype(auto) doMutexProtected(Mutex& mutex, Func&& func, Args&&... args)
{
    std::unique_lock lg(mutex);
    return std::forward<Func>(func)(std::forward<Args>(args)...);
}

这会将互斥锁锁定在 RAII 类型中,因此所有退出路径都会释放互斥锁,然后完美地转发函数,并且它的参数返回与 func 返回的确切类型。

现在,由于您不能使用现代 C++,我们必须尽可能多地尝试实现上述功能,并且有几种方法可以解决问题。实现std::unique_lock 非常简单。根据您想要的功能,它可以很简单

template <typename Mutex>
class my_unique_lock
{
public:
    unique_lock(Mutex& mutex) : mutex(mutex) { mutex.lock(); }
    ~unique_lock() { mutex.unlock(); }
private:
    Mutex& mutex;
    unique_lock(unique_lock const&); // make it non copyable
};

这样就可以解决 25% 的问题 :)。不幸的是,这是最简单的部分。由于C++98/03没有decltype(auto),甚至没有decltypeauto,我们需要想出一种不同的方法来获取返回类型。我们可以将其设为void 并使用输出参数,这意味着您在调用函数时不需要指定任何内容,但这意味着您无法获得对返回内容的引用。以必须指定您想要的返回类型为代价,您可以拥有类似的功能

template <typename Ret, typename Mutex, typename Func, typename Arg1>
Ret doMutexProtected(Mutex& mutex, Func func, Arg1 arg1)
{
    my_unique_lock<Mutex> lg(mutex);
    return func(arg1);
}

你会这样称呼它

T foo = doMutexProtected<T>(mutex, func, arg);
T& bar = doMutexProtected<T&>(mutex, func, arg);

由于 C++98/03 没有可变参数模板,你不得不为不同数量的参数添加一堆重载,你必须决定在哪一点足够的参数是足够的,即:

template <typename Ret, typename Mutex, typename Func, typename Arg1>
Ret doMutexProtected(Mutex& mutex, Func func, Arg1 arg1) {...}

template <typename Ret, typename Mutex, typename Func, typename Arg1, typename Arg2>
Ret doMutexProtected(Mutex& mutex, Func func, Arg1 arg1, Arg2 arg2) {...}

template <typename Ret, typename Mutex, typename Func, typename Arg1, typename Arg2, typename Arg3>
Ret doMutexProtected(Mutex& mutex, Func func, Arg1 arg1, Arg2 arg2, Arg3 arg3) {...}
...

然后你必须处理引用。现代版本完美地转发了所有内容(除非Func 要求,否则不会复制任何内容)。我们不能在 C++98/03 中这样做,所以我们必须添加所有的引用排列,这样我们就不会像第一个版本那样制作不必要的副本。这意味着

template <typename Ret, typename Mutex, typename Func, typename Arg1>
Ret doMutexProtected(Mutex& mutex, Func func, Arg1 arg1)

确实需要

template <typename Ret, typename Mutex, typename Func, typename Arg1>
Ret doMutexProtected(Mutex& mutex, Func& func, Arg1& arg1) {...}

template <typename Ret, typename Mutex, typename Func, typename Arg1>
Ret doMutexProtected(Mutex& mutex, Func const& func, Arg1& arg1)  {...}

template <typename Ret, typename Mutex, typename Func, typename Arg1>
Ret doMutexProtected(Mutex& mutex, Func& func, Arg1 const& arg1)  {...}

template <typename Ret, typename Mutex, typename Func, typename Arg1>
Ret doMutexProtected(Mutex& mutex, Func conts& func, Arg1 const& arg1)  {...}

当您添加更多参数时,它会膨胀。

如果您不想自己做所有这些,我相信Boost 至少已经为 C++03 完成了部分工作,您可以使用它们的实用程序。

【讨论】:

  • 非常完整的答案!很遗憾看到在最近的版本中它是多么容易......一个人只能做梦。谢谢!
【解决方案2】:

让我从字面上看你的例子:

例如,以下内容:

myMutex.acquire();
int a = foo(arg1, arg2, arg3);
myMutex.release();
return a;

您一开始就不应该编写这样的代码。为什么?这不是异常安全的。如果foo 抛出异常怎么办?您将错过释放互斥锁,最终您的程序将永远等待永远不会释放的互斥锁。

避免这种情况的方法是使用RAII,也就是:“析构函数是你的朋友”。如果您不能使用具有 std::scoped_lock 的 C++17,您可以轻松编写自己的作用域锁,甚至可以使用模板来做到这一点:

template <typename mutex_t>
struct my_scoped_lock {
     mutex_t& m;
     scoped_lock(mutex_t& m) : m(m) {m.acquire();}
     ~scoped_lock() { m.release(); }
};

现在你不能忘记释放互斥锁:

int foo( /*...*/ ) {
    my_scoped_lock<mutex_t> lock(myMutex);
    int a = foo(arg1,arg2,arg3);
    return a;
}     

如果您想知道如何编写您实际要求的函数,我建议您参考另一个答案,我只是建议您重新考虑是否真的值得付出努力。

或者一些类似的机制。挑战在于如何做到这一点 任意类型的 a 以及任意类型和数量的参数。

也许这个“其他机制”只是像我上面指出的那样编写一个函数。

PS:我打算扩展这个答案,但与此同时,已经有一个比我能提供的更完整的答案,所以我就把它留在那里。

【讨论】:

  • +1 建议 RAII,考虑到我的措辞方式,这是一个很好的观点。虽然它不一定专门用于资源获取/释放,但更多的是遵循“做一些恒定的事情,然后做一些独特的事情,然后做一些其他的事情”的模式。但是,根据您编写的代码,您的观点可能仍然存在!
  • @Oracular 我觉得您对给定示例的问题过于复杂化了。正如我理解的问题,你的例子并不是你想要写的最好的,所以我决定先专注于这个例子,然后我意识到在我找到你问题的核心之前已经有一个详尽的答案跨度>
  • @Oracular btw RAII 比人们想象的更通用。 “资源”可以是任何东西。例如,您可以编写一个在其构造函数中执行“某些常量”的类,然后在其析构函数中执行“其他常量”,现在您只需在“做一些独特的事情”的范围的开头创建此类的一个实例
【解决方案3】:

一旦你有其他答案所建议的scoped_lock,你就不再需要仿函数了。你可以这样写你的例子:

return scoped_lock<Mutex>(myMutex),
          foo(arg1, arg2, arg3);

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-21
    • 1970-01-01
    • 2020-01-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多