如何使一个函数接受任意数量的参数,而不使用f(...)?

6
一段代码胜过千言万语:
int main()
{
    // All of the following calls return true:
    AreEqual(1, 1);
    AreEqual(1, 1, 1);
    AreEqual(1, 1, 1, 1);
    AreEqual(1, 1, 1, 1, 1);

    // All of the following calls return false:
    AreEqual(1, 2);
    AreEqual(1, 2, 1);
    AreEqual(1, 7, 3, 1);
    AreEqual(1, 4, 1, 1, 1);    
}

如何实现接受任意数量的参数的AreEqual()函数?

一个繁琐但简单的解决方法是通过重载:

bool AreEqual(int v1, int v2);
bool AreEqual(int v1, int v2, int v3);
bool AreEqual(int v1, int v2, int v3, int v4);
......

另一个微不足道但行不通的解决方案是:

bool AreEqual(...);

这种解决方案不可行,因为调用者必须添加另一个参数(参数计数或结束标记)以指定参数的数量。

另一种方法是通过可变模板参数。

template<class... Args>
bool AreEqual(Args... args)
{
    // What should be placed here ???
}

“任意”的意思是“每个整数都是任意类型的(例如,前两个是int,第三个是long)”还是“相同类型的任意数量的参数”? - Utaal
你可以将参数声明为 long long,这样你就可以传递 int、short、long、char、bool 等精度较低的任何类型。 - ApproachingDarknessFish
你为什么要避免使用 f(...) 函数? - bames53
@bames53,这个解决方案不可行,因为调用者必须传递一个参数来指定参数的数量。 - xmllmx
@xmllmx 所以你指的是仅限于可变参数函数 f(...)。它也可以被解释为可变参数模板,因此不太清楚。 - bames53
显示剩余2条评论
5个回答

11

以下是如何使用模板实现它的方法:

#include <iostream>
#include <iomanip>

template<class T0>
bool AreEqual(T0 t0) { return true; }

template<class T0, class T1, class... Args>
bool AreEqual(T0 t0, T1 t1, Args ... args) {
  return t0 == t1 && AreEqual(t1, args...);
}


int main () {
  std::cout << std::boolalpha;

    // All of the following calls return true:
  std::cout<< AreEqual(1, 1) << "\n";
  std::cout<< AreEqual(1, 1, 1) << "\n";
  std::cout<< AreEqual(1, 1, 1, 1) << "\n";
  std::cout<< AreEqual(1, 1, 1, 1, 1) << "\n\n";

    // All of the following calls return false:
  std::cout<< AreEqual(1, 2) << "\n";
  std::cout<< AreEqual(1, 2, 1) << "\n";
  std::cout<< AreEqual(1, 7, 3, 1) << "\n";
  std::cout<< AreEqual(1, 4, 1, 1, 1)  << "\n";
}

您应该考虑这些变化是否适合您的使用:

  • 使用引用而不是按值传递
  • 使非变参模板使用两个参数而不是一个。


或者,这里有一个非递归版本。遗憾的是,它不能短路。如果想看一个非递归短路版本,请查看其他答案。

template<typename T, typename... Args>
bool AreEqual(T first, Args... args)
{
  return std::min({first==args...});
}


最后,这个版本的吸引力在于它缺乏微妙之处:

template<typename T, typename... Args>
bool AreEqual(T first, Args... args)
{
  for(auto i : {args...})
    if(first != i)
      return false;
  return true;
}

它们可能应该是“内联”的。 - Utaal
1
@Utaal:为什么?模板已经有了一种类似于ODR的“异常”。 - GManNickG
@GManNickG 我指的是性能/代码大小,而不是重复定义。如果我没记错的话,AreEqual(1, 4, 1, 1, 1)会扩展为5个非模板“函数”(第一个函数接受5个参数,最后一个函数只接受一个参数)。这意味着,如果没有内联,你需要支付5个函数调用的开销,才能计算5个整数的相等性。我还无法确认这一点,但感觉编译器应该能够内联所有这些调用。 - Utaal
4
如果你的编译器足够好,它将完全独立于你是否使用inline关键字来生成内联代码(或不生成)。 - Robᵩ
@Robᵩ,@GManNickG - 哎呀,我在谷歌上搜索了一下,才意识到我一直在错误地使用 inline。我知道 inline 经常被忽略,但我不知道非 inline 函数也会被内联。 - Utaal
这是正确答案中的第一个,所以我选择它作为最终答案。谢谢! - xmllmx

4

因为某些原因,您似乎排除了明智的方法,您也可以尝试使用std::initializer_list

template<typename T>
bool AreEqual(std::initializer_list<T> list) {
    ...
}

然后你可以这样调用:

AreEqual({1,1,1,1,1});

+1 可以实现优美的实现 return std::adjacent_find(list.begin(), list.end(), std::not_equal_to<T>()) == list.end(); 或者 return std::all_of(std::next(list.begin()), list.end(), std::bind(std::equal_to<T>(), *list.begin(), std::placeholders::_1)); (该实现对第一个元素有特殊处理,因此不太优美,但正确使用了 == 而不是 !=,尽管 ==!= 应该保持同步)。 - Christian Rau

3
这里是非递归版本:
template <typename T> using identity = T;

template<typename T, typename... Args>
bool AreEqual(T first, Args... args)
{
  bool tmp = true;
  identity<bool[]>{tmp?tmp=first==args:true ...};
  return tmp;
}

启用优化后,与递归函数相比,没有额外的开销,但行为可能会有所不同,因为所有参数都与第一个参数进行比较。


1

使用

 bool AreEqual(int v1, ...);

然而,您需要以某种方式检测整数列表的结尾。如果有特定的整数值不合法传递给函数,则可以使用该值。例如,如果所有整数都为正数,则可以使用-1来标记结尾。否则,您可以将第一个参数设置为计数器。
以下是计数版本:
bool AreEqual(int count, int v1, ...)
{
     va_list vl;
     va_start(vl, count);
     for(int i = 1; i < count; ++i)
        if (va_arg(v1, int) != v1)
        {
            va_end(vl);
            return false;
        }
     va_end(vl);
     return true;
}

这里是以结束标记结尾的版本:
bool AreEqual(int v1, ...)
{
     va_list vl;
     va_start(vl, count);

     do
     {
         int param = va_arg(vl, int);
         if (param == -1)
         {
             va_end(vl);
             return true;
        }
    } while (param == v1);
    va_end(vl);
    return false;
}

1
这个解决方案不可行,因为调用者必须传递一个参数来指定参数的数量。 - xmllmx
@xmllmx:那是一个选项。另一个选项是在列表末尾使用标记。 - David Schwartz
1
强制调用者附加标记会改变函数接口。 - xmllmx
1
我同意。但是既然你没有实现(否则,为什么要问这个问题?),那么没有任何代码依赖于该接口。因此,它很容易更改。 - David Schwartz
2
我已经有了一个实现,也就是重载的解决方案。但我感觉它不够优雅,所以我提出了这个问题。 - xmllmx

1
可变参数模板需要在特化时进行递归:
template<class A> //just two: this is trivial
bool are_equal(const A& a, const A& b)
{ return a==b; } 

template<class A, class... Others>
bool are_equal(const A& a, const A& b, const Others&... others)
{ return are_equal(a,b) && are_equal(b,others...); }

实际上,每次嵌套 are_equal 时,others... 将缩短一个,直到消失,并且该函数将绑定两个参数。
注意:通过将 A 用作 a 和 b 的类型,并且因为 Others 的第一个始终要匹配 A,这实际上使得 are_equal(...) 仅接受相同类型(或至少可转换为第一个参数类型)的所有参数。
尽管我发现这种限制通常很有用,但是使用 A 和 B 作为 a 和 b 的类型可以放宽限制。这使得函数能够处理每组类型,其中每个对的 operator== 存在。
template<class A, class B> //just two: this is trivial
bool are_equal(const A& a, const B& b)
{ return a==b; } 

template<class A, class B, class... Others>
bool are_equal(const A& a, const B& b, const Others&... others)
{ return are_equal(a,b) && are_equal(b,others...); }

我认为这个解决方案是最好的。简洁、优雅、美丽!这个答案应该得到更多的赞。 - xmllmx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接