【问题标题】:Can a templated function be a template argument to another function?模板化函数可以作为另一个函数的模板参数吗?
【发布时间】:2017-11-09 16:50:56
【问题描述】:

我以为我会对一些排序算法进行基准测试,但我一定是做错了模板:

代码

#include <iostream>
#include <vector>

template <typename ForwardIterator>
void dummysort(ForwardIterator begin_it, ForwardIterator end_it)
{
    // pretend to use these and sort stuff
    ++begin_it;
    ++end_it;
}

template<typename SortFunc>
void benchmark(const char* name, SortFunc sort_func, std::vector<int> v)
{
    std::cout << name << std::endl;
    sort_func(v.begin(), v.end());
}

int main()
{
    std::vector<int> first = {3, 2, 1};

    benchmark("bubblesort", dummysort, first);
}

错误

10:48 $ clang -std=c++14 tmp.cpp
tmp.cpp:30:5: error: no matching function for call to 'benchmark'
    benchmark("bubblesort", dummysort, first);
    ^~~~~~~~~
tmp.cpp:20:6: note: candidate template ignored: couldn't infer template argument 'SortFunc'
void benchmark(const char* name, SortFunc sort_func, std::vector<int> v)
     ^
1 error generated.

编译器信息

10:47 $ clang --version
Apple LLVM version 8.1.0 (clang-802.0.42)
Target: x86_64-apple-darwin16.0.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/

如果我“取消模板化”dummysort,它会起作用。

void dummysort(std::vector<int>::iterator begin_it, std::vector<int>::iterator end_it)

问题

有什么方法可以让它通用,或者,如果没有,有人可以给我一个很好的解释或类似于this answer的思想实验吗?

【问题讨论】:

  • 不能传递模板,只能传递函数。你应该可以写benchmark("bubblesort", &amp;dummysort&lt;int&gt;, first);。如果您希望推断出int,您也可以通过编写一个根据类型为您提供正确重载的包装器来做到这一点。
  • @nwp 所以我不能编写一个基准,它采用与 std::sort 兼容的参数的通用排序函数?
  • @nwp - 你是说&amp;dummysort&lt;std::vector&lt;int&gt;::iterator&gt; 吗?
  • @Ben - 考虑一下,在 C++ 中,模板函数不是函数:是构造函数的秘诀;当您传递dummisort 时,您传递的是配方,而不是函数;如果你想传递一个函数,你必须明确类型(如 nwp 所建议的那样);但还有另一种方法......我会尝试写一个例子并准备一个答案。
  • @max66 是的,这更有意义。

标签: c++ templates c++14 template-meta-programming


【解决方案1】:

正如 nwp 所解释的,模板函数不是函数。只是构造函数的秘诀。

所以你不能通过配方

benchmark("bubblesort", dummysort, first);

但是您可以通过说明类型来从配方中构造函数;我是说

benchmark("bubblesort", dummysort<std::vector<int>::iterator>, first);

有效,但我觉得它有点难看。

我建议你另一种解决方案:代替模板函数,构造一个类(或结构),其中包含模板operator();像

struct dummySort
 {
   template <typename FI>
   void operator() (FI begin_it, FI end_it)
    {
      ++begin_it;
      ++end_it; 
    }
 };

通过这种方式,您可以调用 benchmark() 并传递此类型的对象,而无需说明迭代器的类型:这是对 benchmark() 内部的运算符的调用选择正确的类型并构造正确的运算符。

以下是完整的编译示例

#include <iostream>
#include <vector>

struct dummySort
 {
   template <typename FI>
   void operator() (FI begin_it, FI end_it)
    { ++begin_it; ++end_it; }
 };

template <typename SortFunc>
void benchmark (char const * name, SortFunc sort_func, std::vector<int> v)
 {
   std::cout << name << std::endl;
   sort_func(v.begin(), v.end());
 }

int main()
 {
   std::vector<int> first = {3, 2, 1};

   benchmark("bubblesort", dummySort{}, first);
 }

【讨论】:

  • 也可以通过将模板化函数包装在 lambda 中来延迟它的实例化:benchmark("bubblesort", dummysort&lt;std::vector&lt;int&gt;::iterator&gt;, first); 可以替换为 benchmark("bubblesort", [](auto beg, auto end){dummysort(beg, end);}, first); 更多信息可以在这里找到:fluentcpp.com/2017/08/01/overloaded-functions-stl
  • @sv90 好牛啊
【解决方案2】:

有几种方法可以解决这个问题。

  1. 您可以为dummysort 显式指定模板参数,也可以将static_cast 指定为适当的函数指针类型。对我来说,这是一个非常糟糕的解决方案,因为它锁定了特定的重载选择,而不是依赖于语言的重载规则。如果您更改其他参数或类型,您可能会得到(最佳情况下)损坏的代码或(最坏情况下)有效但做一些意外的代码

  2. 你可以将你的函数模板提升为一个函数object,它有一个调用操作符模板:

    struct dummysort_t {
        template <typename ForwardIterator>
        void operator()(ForwardIterator begin_it, ForwardIterator end_it) const {
            /* ... */
        }
    } constexpr dummysort{}; // in C++17, also mark inline
    

    这使您可以直接将dummysort 传递给您的函数模板,并且具有相同的预期行为。此解决方案的缺点是,如果您不拥有 dummysort,这当然是不可行的,如果您使用 dummysort 作为自定义点,则需要做更多的工作才能做到这一点有预期的行为。

  3. 您可以将其包装在 lambda 中。由于我们要复制迭代器,所以简单的形式就可以了:

    [](auto b, auto e) { dummysort(b, e); }
    

    这会延迟 dummysort 的模板实例化,直到另一个函数模板实际调用它。这为您提供了您想要的确切行为,并且适用于所有情况(并解决了#2 的问题)。更一般地说,这种模式应该像这样使用:

    #define OVERLOADS_OF(name) [&](auto&&... args) -> decltype(name(std::forward<decltype(args)>(args)...)) { return name(std::forward<decltype(args)>(args)...); }
    

    你会通过的:

    OVERLOADS_OF(dummysort)
    

    这对于这种特殊情况来说是多余的,但更普遍有用。

【讨论】:

    【解决方案3】:

    两个次要 cmets:您应该通过引用 std::vector&lt;int&gt; &amp;v 而不是通过值 std::vector&lt;int&gt; vv 参数传递给 benchmark,因为向量是构建起来昂贵的对象。标准流不需要std::endl;而不是

    std::cout << "Value of the variable:" << std::endl;
    std::cout << variable << std::endl;
    

    你可以写

    std::cout << "Value of the variable:\n";
    std::cout << variable << '\n';
    

    这既不那么冗长又快得多(这对于这里的琐碎案例并不重要)。

    要回答您的问题,不,在这种情况下编译器将无法推断dummysort 的模板参数,因此您必须将其编写为dummysort&lt;std::vector&lt;int&gt;::iterator&gt;。但是有一些方法可以简化它:

    1) 如果你的编译器支持 C++14 variable templates 你可以声明

    template<typename T>
    constexpr auto vecdummysort = dummysort<typename std::vector<T>::iterator>;
    

    然后像这样使用它

    benchmark("bubblesort", vecdummysort<int>, first);
    

    2a) 使用 C++11,您可以创建 alias template

    template<typename T>
    using veciter = typename std::vector<T>::iterator;
    

    这让您可以使用 dummysort 喜欢

    benchmark("bubblesort", dummysort<veciter<int>>, first);
    

    2b) 对于与示例 #1 中相同的 vecdummysort 函数:

    template<typename T>
    void vecdummysort(veciter<T> first, veciter<T> last) {dummysort(first, last);}
    

    3) 您也可以在benchmark 中更改对sort_func 的调用以使用原始指针而不是迭代器,所以

    sort_func(v.data(), v.data() + v.size());
    

    而不是

    sort_func(v.begin(), v.end());
    

    这意味着您可以使用benchmark 喜欢

    benchmark("bubblesort", dummysort<int*>, first);
    

    4) 您可以更改dummysort 的接口以将集合本身​​作为参数而不是迭代器,这会牺牲一点灵活性(例如,如果您只想对集合的一部分进行排序,或者使用反向迭代器,等等)以获得更简洁和更符合人体工程学的界面

    template<typename Collection>
    void dummysort(Collection &c)
    {
        auto first = std::begin(c);
        auto last = std::end(c);
    }
    

    用法:

    // benchmark()
    sort_func(v);
    // main()
    benchmark("bubblesort", dummysort<std::vector<int>>, first);
    

    【讨论】:

    • 我希望容器按值传递,因为我想使用不同的排序函数多次调用基准,并且我不希望一个基准影响下一个基准。
    • 谢谢,我没有意识到我可以在呼叫站点专门化我的功能并使用这样的别名模板。这个答案让我可以用 std::sort 可交换地编写我的排序函数,我只需要在基准测试中输入更多内容
    • @Ben 您仍然应该通过引用传递容器(以及 std::shuffle 它在基准测试之间),原因有两个。 1) 在幕后,动态分配获取和释放互斥体,并可能触发页面错误,这两者都可以在像 C++ 这样的低级非 GC 语言中抛出基准(您可以改为通过 std::array value), 2) 如果编译器可以证明没有观察到的副作用取决于向量的存在,它可以删除它并将排序作为死代码!方便的是,std::shuffle 使用 RNG 是一种副作用。
    • 其他观察到的副作用包括,在每次排序之前和之后,打印向量的每个元素,或将每个元素分配给 volatile 变量。 (你需要做每一个元素,因为如果你只是打印,比如说,v.back(),编译器可能会推断它只需要找到最大值,不需要对其余部分进行排序)。但我认为std::shuffle 更简洁(确保在第一个基准测试之前以及其他基准测试之前执行此操作),并且可能更好地防止优化编译器的聪明之处,它内联然后在编译时对排序算法进行常量折叠。跨度>
    • 感谢您关注 the start of my benchmark code 。这是在我决定将迭代器传递给排序函数之前。无论如何,直到函数启动之后我才开始基准测试,并且在之前和之后打印它,所以我希望我在页面错误方面是安全的。我会更担心 std::shuffle 的任何影响,这意味着我正在测试不同的数据
    猜你喜欢
    • 1970-01-01
    • 2021-11-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多