【问题标题】:Typesafe variadic function类型安全的可变参数函数
【发布时间】:2018-10-25 11:37: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&lt;const char *&gt; args) 的选项,但这使得我想避免调用foo({"hello", "world"});

【问题讨论】:

  • 您是否与调用语法foo("hello", "world"); 绑定在一起?如果不是,那么 std::initializer_list&lt;const char *&gt; 是合适的参数类型
  • @gsamaras 我想要的语法根本不起作用。如果T args... 的意思是“std::initializer_list&lt;T&gt; 已从所有剩余参数初始化,则否。
  • @Caleth - 我已经更新了问题:我想避免 ({})(但感谢您澄清我的要求)
  • 回复:“担心 [它] 会让错误变得混乱”——试试吧。

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


【解决方案1】:

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

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");
}

【讨论】:

  • 这对于它所取得的成就来说是相当沉重的......更不用说完全滥用enable_if 与指定参数的类型。
  • @rubenvb 如何指定相同类型的可变参数数量?我不明白你所说的“滥用”是什么意思。
  • 查看我的答案以及@Nevin's。
  • @Deduplicator 然后使用std::is_convertible_v。目前尚不清楚他们是否想要隐式转换。甚至不清楚他们是否想要一个硬错误或 SFINAE。这就是为什么我的答案以 我想你可能想要这样的东西开头
【解决方案2】:

注意:不可能只匹配字符串文字。最接近的方法是匹配 const char 数组。

要进行类型检查,请使用带有 const char 数组的函数模板。

要使用基于范围的for 循环它们,我们需要将其转换为initializer_list&lt;const char*&gt;。我们可以直接在基于范围的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
    }
}

【讨论】:

  • 非常好...一个小建议:在问题中的“我想写什么,类似于”代码中添加一个解决方案示例。
  • @max66,根据 OP 对语法的请求,我更新了代码和解释以使用基于范围的 for。
【解决方案3】:

虽然所有其他答案都可以解决问题,但您也可以执行以下操作:

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 of ‘void 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>

【讨论】:

    【解决方案4】:

    +1 用于 C++17 liliscent 的解决方案。

    对于 C++11 解决方案,一种可能的方法是创建一个类型特征来生成多个值的“与”(类似于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)。

    【讨论】:

    • 赞成。值得一提的是,这其实是C++17中std::conjunction的一个实现。
    • @liliscent - std::conjunction!我记得有类似的东西,但名字并没有出现在我的脑海中。谢谢。
    • multAnd 也可以这样实现:template &lt;bool...&gt; struct Bools{}; template &lt;bool... Bs&gt; struct all_of : std::is_same&lt;Bools&lt;true, Bs...&gt;, Bools&lt;Bs..., true&gt;&gt; {};
    • @Jarod42 - 我不知道我是怎么想到的:std::is_same 检查移位列表是我最喜欢的技巧之一。谢谢。
    • 禁止所有隐式转换有点不自然。
    【解决方案5】:

    在逗号运算符上使用C++17 fold expressions,您可以简单地执行以下操作:

    #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"));
    }
    

    Live demo here。注意我在模板中使用了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");
    }
    

    Live demo here.

    请注意,这与 C++11 语法非常接近,以实现完全相同的目标。参见例如this question of mine.

    【讨论】:

    • 您刚刚删除了enable_if。而你的回答并没有回答问题,这个问题特别需要一个for 循环。
    • 我的代码就像写了一个 for 循环一样工作,减去了 initializer_list 中参数的临时副本。
    • 您的第一个版本解决了无法写入for 循环的异构类型。你的第二个版本也是异构的,更不用说OP想要const char*。我不会说这很糟糕,但你对我的代码的批评太过分了。
    • @liliscent 我并不特别想要一个 for 循环——这正是我在示例中所写的。抱歉,不清楚。
    【解决方案6】:

    好吧,你可以得到最接近的函数,它接受任意数量的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)...});
    }
    

    微妙之处在于允许正常的隐式转换。

    【讨论】:

      【解决方案7】:
      #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
      }
      

      【讨论】:

        【解决方案8】:

        而现在……完全不同的东西……

        你可以写一个类型包装结构如下

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

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

        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
         }
        

        【讨论】:

          【解决方案9】:

          当然可以,这样编译运行你想要的(注意)

          #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 &lt;class... Args&gt;。我个人觉得将代码中有趣的部分放在只能通过滚动条才能看到的“幽默”非常可笑。
          • 不,他们没有 - 但他们没有假装。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-09-23
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多