类型安全的可变参数函数

16

我希望编写一个函数,可以接受可变数量的字符串字面值参数。如果我使用C语言编写,可能需要编写类似以下的代码:

void foo(const char *first, ...);

然后调用将会是这个样子:

foo( "hello", "world", (const char*)NULL );

感觉 C++ 应该可以做得更好。我想到的最好的方法是:

template <typename... Args>
void foo(const char* first, Args... args) {
    foo(first);
    foo(args);
}

void foo(const char* first) { /* Do actual work */ }

称为:

foo("hello", "world");

但我担心递归性质和我们在只有一个参数时才进行任何类型检查的事实,会使得如果某人调用foo("bad", "argument", "next", 42)将导致错误难以理解。我想要写的是:

void foo(const char* args...) {
    for (const char* arg : args) {
        // Real work
    }
}

有什么建议吗?

编辑:还有一个选项是void fn(std::initializer_list<const char *> args),但这会使调用变成foo({"hello", "world"});,而我想避免这种情况。


1
你是否被调用语法 foo("hello", "world"); 所束缚?如果不是,那么 std::initializer_list<const char *> 就是一个合适的参数类型。 - Caleth
3
这难道不会触发“参数包没有通过'...'展开”的错误吗? - gsamaras
@gsamaras我的期望语法根本无法工作。如果T args...意味着从所有剩余参数初始化的“std::initializer_list<T>”,那么不行。 - Martin Bonner supports Monica
@Caleth - 我已经更新了问题:我想避免 ({})(但感谢您澄清我的要求) - Martin Bonner supports Monica
回复:"害怕会出错或混淆" - 試試看。 - Pete Becker
显示剩余3条评论
9个回答

15

我认为你可能想要这样的东西:

template<class... Args,
    std::enable_if_t<(std::is_same_v<const char*, Args> && ...), int> = 0>
void foo(Args... args ){
    for (const char* arg : {args...}) {
        std::cout << arg << "\n";
    }
}

int main() {
    foo("hello", "world");
}

1
这对于它所实现的功能来说相当沉重...更不用说完全滥用enable_if而不是指定参数类型了。 - rubenvb
1
@rubenvb 你如何指定相同类型的可变数量参数?我不明白你所说的“滥用”是什么意思。 - llllllllll
1
请查看我的回答以及@Nevin的回答。 - rubenvb
3
那么请使用 std::is_convertible_v。不清楚他们是否需要隐式转换,甚至不确定他们是想要一个硬错误还是 SFINAE。这就是为什么我的回答以 我认为你可能想要这样做 开始的原因。 - llllllllll

8
注意:不可能只匹配字符串文字。最接近的方法是匹配const char数组。
为了进行类型检查,使用一个函数模板来接受const char数组。
要使用基于范围的for循环遍历它们,我们需要将其转换为initializer_list。我们可以在基于范围的for语句中直接使用大括号进行转换,因为数组会衰减为指针。
以下是函数模板的示例(注意:此模板适用于零个或多个字符串文字。如果您想要一个或多个,请更改函数签名以至少接受一个参数):
template<size_t N>
using cstring_literal_type = const char (&)[N];

template<size_t... Ns>
void foo(cstring_literal_type<Ns>... args)
{
    for (const char* arg : {args...})
    {
        // Real work
    }
}

非常好...有一个小建议:在问题中的"What I want to write, is something like"代码上添加您解决方案的示例。 - max66
@max66,我已经更新了代码和解释,使用了基于范围的for循环,以符合原帖请求的语法。 - Nevin

4

虽然其他答案已经解决了问题,但是你也可以采取以下方法:

namespace detail
{
    void foo(std::initializer_list<const char*> strings);
}

template<typename... Types>
void foo(const Types... strings)
{
    detail::foo({strings...});
}

这种方法(至少在我看来)似乎比使用SFINAE更易读,并且适用于C++11。此外,它允许您将foo的实现移到cpp文件中,这也可能很有用。
编辑:至少在GCC 8.1中,我的方法似乎在使用非const char*参数时产生更好的错误消息。
foo("a", "b", 42, "c");

此实现与以下版本兼容:

test.cpp: In instantiation ofvoid foo_1(const ArgTypes ...) [with ArgTypes = {const char*, int, const char*, const char*}]’:
test.cpp:17:29:   required from here
test.cpp:12:16: error: invalid conversion from ‘int’ to ‘const char*’ [-fpermissive]
 detail::foo({strings...});
 ~~~~~~~~~~~^~~~~~~~~~~~~~

虽然基于SFINAE的方法(liliscent的实现)可以产生以下结果:
test2.cpp: In function ‘int main()’:
test2.cpp:14:29: error: no matching function for call to ‘foo(const char [6], const char [6], int)’
     foo("hello", "world", 42);
                         ^
test2.cpp:7:6: note: candidate: ‘template<class ... Args, typename std::enable_if<(is_same_v<const char*, Args> && ...), int>::type <anonymous> > void foo(Args ...)’
 void foo(Args... args ){
  ^~~
test2.cpp:7:6: note:   template argument deduction/substitution failed:
test2.cpp:6:73: error: no type named ‘type’ in ‘struct std::enable_if<false, int>’
     std::enable_if_t<(std::is_same_v<const char*, Args> && ...), int> = 0>

2

对于C++17的liliscent解决方案,我给予+1的支持。

对于C++11的解决方案,一种可能的方法是创建一个类型特征来将多个值进行“and”操作(类似于std::conjunction,但不幸的是,它只能从C++17开始使用...当您可以使用折叠表达式时,您就不再需要std::conjunction了(感谢liliscent))。

template <bool ... Bs>
struct multAnd;

template <>
struct multAnd<> : public std::true_type
 { };

template <bool ... Bs>
struct multAnd<true, Bs...> : public multAnd<Bs...>
 { };

template <bool ... Bs>
struct multAnd<false, Bs...> : public std::false_type
 { };

所以foo()可以写成

template <typename ... Args>
typename std::enable_if<
      multAnd<std::is_same<char const *, Args>::value ...>::value>::type
   foo (Args ... args )
 {
    for (const char* arg : {args...}) {
        std::cout << arg << "\n";
    }
 }

使用C++14,multAnd()可以被写成一个constexpr函数。

template <bool ... Bs>
constexpr bool multAnd ()
 {
   using unused = bool[];

   bool ret { true };

   (void)unused { true, ret &= Bs ... };

   return ret;
 }

所以foo()变成了

template <typename ... Args>
std::enable_if_t<multAnd<std::is_same<char const *, Args>::value ...>()>
   foo (Args ... args )
 {
    for (const char* arg : {args...}) {
        std::cout << arg << "\n";
    }
 }

---编辑---

Jarod42(感谢!)提出了一种更好的开发multAnd的方法,如下所示:

template <typename T, T ...>
struct int_sequence
 { };

template <bool ... Bs>
struct all_of : public std::is_same<int_sequence<bool, true, Bs...>,
                                    int_sequence<bool, Bs..., true>>
 { };

从C++14开始,可以使用std::integer_sequence代替它的模拟(int_sequence)。


1
点赞。值得一提的是,这实际上是C++17中std::conjunction的一个实现。 - llllllllll
@liliscent - std::conjunction!我记得有类似的东西,但名字想不起来了。谢谢。 - max66
1
multAnd can be implemented that way too: template <bool...> struct Bools{}; template <bool... Bs> struct all_of : std::is_same<Bools<true, Bs...>, Bools<Bs..., true>> {}; - Jarod42
@Jarod42 - 我不知道为什么我没有想到:在移位列表上进行std::is_same检查是我最喜欢的技巧之一。谢谢。 - max66
禁止所有隐式转换有些不自然。 - Deduplicator

1

使用C++17 折叠表达式来操作逗号运算符,你只需要简单地执行以下操作:

#include <iostream>
#include <string>
#include <utility>

template<typename OneType>
void foo_(OneType&& one)
{
    std::cout << one;
}

template<typename... ArgTypes>
void foo(ArgTypes&&... arguments)
{
    (foo_(std::forward<ArgTypes>(arguments)), ...);
}

int main()
{
    foo(42, 43., "Hello", std::string("Bla"));
}

这里有实时演示。注意,我在模板中使用了foo_,因为我懒得写出 4 个重载。


如果你真的非常想将其限制为字符串字面值,按照Nevin的答案所建议,更改函数签名:

#include <cstddef>
#include <iostream>
#include <string>
#include <utility>

template<std::size_t N>
using string_literal = const char(&)[N];

template<std::size_t N>
void foo(string_literal<N> literal)
{
    std::cout << literal;
}

template<std::size_t... Ns>
void foo(string_literal<Ns>... arguments)
{
    (foo(arguments), ...);
}

int main()
{
    foo("Hello", "Bla", "haha");
}

这里有实时演示

请注意,这与C++11语法非常接近,可以实现完全相同的功能。例如,请参见我的这个问题


1
你刚刚移除了 enable_if。而且你的回答并没有回答问题,问题明确要求使用 for 循环。 - llllllllll
1
我的代码的工作方式就像写了一个 for 循环一样,除了在 initializer_list 中没有临时复制参数。 - rubenvb
你的第一个版本处理了不同类型,但是不能写入for循环。你的第二个版本也是不同类型的,更何况OP想要const char*。我不会说这是不好的,但你对我的代码进行负面评价有些过分了。 - llllllllll
2
@liliscent,我并不特别想要一个for循环 - 那只是我在例子中写的。对于缺乏清晰度,我感到抱歉。 - Martin Bonner supports Monica

1

好的,最接近能够接受任意数量的const char*但不接受其他类型的函数是使用模板函数和转发:

void foo_impl(std::initializer_list<const char*> args)
{
    ...
}

template <class... ARGS>
auto foo(ARGS&&... args)
-> foo_impl({std::forward<ARGS>(args)...})
{
    foo_impl({std::forward<ARGS>(args)...});
}

细微之处在于允许普通的隐式转换。

0
#include<type_traits>
#include<iostream>

auto function = [](auto... cstrings) {
    static_assert((std::is_same_v<decltype(cstrings), const char*> && ...));
    for (const char* string: {cstrings...}) {
        std::cout << string << std::endl;
    }
};

int main(){    
    const char b[]= "b2";
    const char* d = "d4";
    function("a1", b, "c3", d);
    //function(a, "b", "c",42); // ERROR
}

0

现在...来点完全不同的...

你可以编写一个类型包装结构体,如下所示

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

template <typename U, typename T>
using wrp_t = typename wrp<U, T>::type;

一个接收可变参数列表char const *foo()函数就变成了

template <typename ... Args>
void foo (wrp_t<Args, char const *> ... args)
 {
   for ( char const * arg : {args...} )
      std::cout << "- " << arg << std::endl;
 }

问题在于你不能随意调用它。
foo("hello", "world");

因为编译器无法推断Args...类型。

显然,您可以明确列出虚拟类型列表。

 foo<void, void>("hello", "world");

但是我明白这是一个糟糕的解决方案。

不过,如果你愿意通过一个简单的模板函数来处理

template <typename ... Args>
void bar (Args ... args)
 { foo<Args...>(args...); }

你可以调用

bar("hello", "world");

以下是一个完整的 C++11 工作示例。
#include <iostream>

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

template <typename U, typename T>
using wrp_t = typename wrp<U, T>::type;

template <typename ... Args>
void foo (wrp_t<Args, char const *> ... args)
 {
   for ( char const * arg : {args...} )
      std::cout << "- " << arg << std::endl;
 }

template <typename ... Args>
void bar (Args ... args)
 { foo<Args...>(args...); }

int main ()
 {
   bar("hello", "world"); // compile

   // bar("hello", "world", 0);  // compilation error
 }

-2

当然可以,这个编译并运行你想要的内容(注意)

#include<iostream>
                                                                                          template<class... Char>
                                                                                          // hehe, here is the secret      
auto foo(const Char*... args )                                                            ->decltype((char const*)(*std::begin({args...})), (char const*)(*std::end({args...})), void(0))
{
    for (const char* arg : {args...}) {
        std::cout << arg << "\n";
    }
}

int main() {
    foo("no", "sense","of","humor");
}

这是@liliscent的解决方案,但加入了更多的糖,并且为了取悦@rubenvb,没有使用enable_if。 如果您认为额外的代码是注释(实际上不是),请注意您将看到您要寻找的完全语法。 请注意,您只能提供可转换为char const*的同质事物列表,这似乎是您的其中一个目标。

这不是我要找的语法 - 它有一个前置 template <class... Args>。我个人认为将代码的有趣部分放在只能通过滚动条才能看到的位置,这种“幽默”非常无趣。 - Martin Bonner supports Monica
不,他们没有这样做 - 但他们也没有假装过。 - Martin Bonner supports Monica

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