【问题标题】:Test if all elements are equal with C++17 fold-expression用 C++17 fold-expression 测试所有元素是否相等
【发布时间】:2017-10-18 08:53:03
【问题描述】:

我有一个采用可变参数包的函数,一开始我想检查所有元素是否比较相等。我可以以某种方式使用新的 C++17 折叠表达式将其简洁地写成单行吗?我在想

template<typename... Args>
void func (Args... args)
{
    ASSERT ((args == ...));

    // more code here...
}

但这不起作用,因为它编译为首先正确比较最后两个参数的代码,然后将倒数第三个参数与第一个比较的结果进行比较,这是一个布尔值。这种类型的折叠表达式可能有什么用例(args &lt; ... 类似)?有没有机会我可以避免编写一个专用的递归模板来做到这一点?

【问题讨论】:

  • ((arg == args) &amp;&amp; ...) 如果你可以在函数参数列表前加上arg

标签: c++ templates variadic-templates c++17 fold-expression


【解决方案1】:

不幸的是,这不起作用的原因是布尔运算符在 C++ 中不像在其他语言中那样链接。所以表达式:

a == (b == c)

(您的折叠表达式将扩展到什么)会将atruefalse 进行比较,与bc 的实际含义无关。我希望operator&lt;=&gt; 会添加链接,但显然该部分已被删除。

解决方法是你必须打破比较:

(a == b) && (b == c)

当然,这并不适合折叠,但您可以将所有内容与第一个元素进行比较:

(a == b) && (a == c)

也就是说:

((a0 == args) && ... )

此时,我们只需要能够拉出第一个元素。没问题,这显然是 lambdas 的用途:

template <class... Args>
constexpr bool all_equal(Args const&... args) {
    if constexpr (sizeof...(Args) == 0) {
        return true;
    } else {
        return [](auto const& a0, auto const&... rest){
            return ((a0 == rest) && ...);
        }(args...);
    }
}

【讨论】:

    【解决方案2】:

    根据 Piotr Skotnicki 的建议,一个简单的解决方案是将第一个参数与以下参数分开,并使用 &amp;&amp; 作为折叠运算符进行检查

    例如,如果所有参数都相等,则以下函数返回 true

    template <typename A0, typename ... Args>
    bool foo (A0 const & a0, Args const & ... args)
     { return ( (args == a0) && ... && true ); } 
    

    很遗憾,这不适用于空的参数列表

    std::cout << foo(1, 1, 1, 1) << std::endl; // print 1
    std::cout << foo(1, 1, 2, 1) << std::endl; // print 0
    std::cout << foo() << std::endl;           // compilation error
    

    但您可以添加特殊的空参数foo()

    bool foo ()
     { return true; }
    

    如果由于某种原因,您无法将args 拆分为a0 和以下args

    嗯...你显然可以使用前面的foo()函数(带有特殊的空版本)

    template<typename... Args>
    void func (Args... args)
    {
        ASSERT (foo(args));
    
        // more code here...
    }
    

    或者您可以使用带有逗号运算符和赋值的 C++17 折叠表达式,如下面的bar()

    template <typename ... Args>
    bool bar (Args const & ... args)
     {
       auto a0 = ( (0, ..., args) );
       return ( (args == a0) && ... && true ); 
     }
    

    注意a0 赋值中的初始零,它允许使用此解决方案也可以使用空参数列表。

    不幸的是,从前面的auto a0 分配中,我得到了很多我不知道如何警告的警告(“表达式结果未使用”,来自 clang++,以及“逗号运算符的左操作数无效”,来自 g++)避免。

    以下是一个完整的工作示例

    #include <iostream>
    
    template <typename A0, typename ... Args>
    bool foo (A0 const & a0, Args const & ... args)
     { return ( (args == a0) && ... && true ); }
    
    bool foo ()
     { return true; }
    
    template <typename ... Args>
    bool bar (Args const & ... args)
     {
       auto a0 = ( (0, ..., args) );
       return ( (args == a0) && ... && true ); 
     }
    
    int main ()
     {
       std::cout << foo(1, 1, 1, 1) << std::endl; // print 1
       std::cout << foo(1, 1, 2, 1) << std::endl; // print 0
       std::cout << foo() << std::endl;           // print 1 (compilation error
                                                  //          witout no argument
                                                  //          version)
    
       std::cout << bar(1, 1, 1, 1) << std::endl; // print 1
       std::cout << bar(1, 1, 2, 1) << std::endl; // print 0
       std::cout << bar() << std::endl;           // print 1 (no special version)
     }
    

    -- 编辑--

    正如 dfri 所指出的(谢谢!),为空 args... 包,以下折叠表达式的值

    ( (args == a0) && ... )
    
    ( (args == a0) || ... )
    

    分别是truefalse

    所以foo()bar()的返回指令可以写成无所谓

     return ( (args == a0) && ... && true );
    

     return ( (args == a0) && ... );
    

    sizeof...(args) == 0U 也是如此。

    但我倾向于忘记这类细节,更喜欢显式(使用最后的&amp;&amp; true)空值。

    【讨论】:

    • 请注意,在空包上进行一元折叠的值,特别是使用&amp;&amp;,是true(相反,false 对应于||),所以bar(...) 的回报(return ( (args == a0) &amp;&amp; ... &amp;&amp; true );) 后面不需要true,可以简化为return ( (args == a0) &amp;&amp; ...);。此外,“观察a0 赋值中的初始零,它允许使用此解决方案也可以使用空参数列表。” a0 的初始值一个“元素”与args 包相比(允许比较相邻元素)?
    • @dfri - 关于&amp;&amp;true:是的,我(现在)知道这一点,但我倾向于忘记这类细节;我认为最好(对我和其他像我一样健忘的人)明确默认值(我想编译器会优化并删除最终的&amp;&amp; true);但你是对的,也许我会添加评论。关于“初始零”......对不起,但我不明白(我的错,我想)你是什么意思)。
    • 关于第二部分:可能是我应该说“我的错”,因为我不太明白我引用的那句话:0 值 w.r.t 的意义是什么?可以用空包(bar())打电话给bar(...)吗?据我了解,最初的0 值(也可能是-42)主要用于折叠表达式比较初始包的每个“相邻元素”(这样a0 本身就是一个“移位- by-one”参数包)。但我觉得我可能错过了这个逻辑。
    • @dfri - 要了解初始零的逻辑,请尝试编译我删除它的完整示例(使用 auto a0 = ( (..., args) );)。从bar() 空调用(最后std::cout 行)中,您应该获得类似于“错误:变量具有不完整类型'void'”的错误。而现在想想如何扩展一个空包。用初始零,扩展为auto as = 0;,即正确编译。没有初始零,扩展为auto as = ;,这是错误的,会报错。
    • 我明白了,谢谢;所以我们实际上是根据0 文字推断的类型(而我们从不使用它的值)以防空包。在这种情况下,来自&lt;optional&gt;std::nullopt 是否更可取(对于语义)?
    【解决方案3】:

    这是我在 gcl 库中的做法:

    template <auto ... values>
    constexpr static auto equal_v = []() consteval {
        static_assert(sizeof...(values) > 0, "gcl::mp::value_traits::equal_v : no arguments");
        constexpr auto first_value = std::get<0>(std::tuple{values...});
        static_assert(
                (std::equality_comparable_with<decltype(values), decltype(first_value)> && ...),
                "gcl::mp::value_traits::equal_v : cannot compare values");
        return ((values == first_value) && ...); 
    }();
    

    或将static_assert 替换为概念要求:

    template <typename ... Ts>
    concept are_equality_comparable = requires(Ts ... values)
    {
        { 
            std::conditional_t<(std::equality_comparable_with<decltype(std::get<0>(std::tuple{values...})), decltype(values)> && ...), std::true_type, std::false_type>{}
        } -> std::same_as<std::true_type>;
    };
    
    template <auto ... values>
        requires(are_equality_comparable<decltype(values)...>)
    constexpr static auto equal_v = []() consteval {
        static_assert(sizeof...(values) > 0, "gcl::mp::value_traits::equal_v : no arguments");
        constexpr auto first_value = std::get<0>(std::tuple{values...});
       
        return ((values == first_value) && ...); 
    }();
    

    【讨论】:

      猜你喜欢
      • 2013-03-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-12-15
      • 2015-02-12
      相关资源
      最近更新 更多