为什么一些标准运算符没有标准的函数对象?

24
我们拥有以下 functors:
  • std::plus (+)
  • std::minus (-)
  • std::multiplies (*)
  • std::divides (/)
  • std::modulus (%)
  • std::negate (-)
  • std::logical_or (||)
  • std::logical_not (!)
  • std::logical_and (&&)
  • std::equal_to (==)
  • std::not_equal_to (!=)
  • std::less (<)
  • std::greater (>)
  • std::less_equal (<=)
  • std::greater_equal (>=)

我们没有以下 functors:

  • & (取地址)
  • * (解引用)
  • []
  • ,
  • 按位运算符 ~, &, |, ^, <<, >>
  • ++ (前缀/后缀) / -- (前缀/后缀)
  • sizeof
  • static_cast / dynamic_cast / reinterpret_cast / const_cast
  • C语言风格的强制类型转换
  • new / new[] / delete / delete[]
  • 所有成员函数指针运算符
  • 所有复合赋值运算符。
  • 我们没有这些操作符,是有原因的吗?还是只是疏忽了?


    1
    我已经重新格式化了你的问题,将运算符放在一个列表中。如果你不介意的话。 - In silico
    @In silico:完全不介意。 - Billy ONeal
    1
    实际上,我对那些缺失的东西感到惊讶不如对那些进入标准库的东西感到惊讶。显然,再次证明了永远不要低估委员会效应的力量。 - 6502
    1
    @6502:我至少期望位运算符和解引用。 (另外,既然它们有完全冗余的std :: greater(例如,“std :: not1(std :: less_equal)”就足够了),为什么不使用“!=”) - Billy ONeal
    2
    C++中最有用的运算符:一元加号。;-) - James McNellis
    @James:哈哈,我以为那只是字面上的一部分。我想每天都能学到新东西。我觉得那个运算符甚至不应该存在 :P - Billy ONeal
    6个回答

    9
    我认为这个问题最可能的答案是,包含的运算符是被认为最有用的。如果没有人想要将某些内容添加到标准库中,它就不会被添加。
    我认为断言C++0x中的运算符函数无用,因为lambda表达式更加优越,这是愚蠢的:确实,lambda表达式很棒而且 更加灵活,但有时使用命名的函数对象可以导致更简洁、更清晰、更易于理解的代码;此外,命名的函数对象可以是多态的,而lambda则不能。
    当然,标准库运算符函数对象不是多态的(它们是类模板,所以操作数类型是函数对象类型的一部分)。不过编写自己的运算符函数对象并不是特别困难,而宏使这项任务非常简单:
    namespace ops
    {
        namespace detail
        {
            template <typename T>
            T&& declval();
    
            template <typename T>
            struct remove_reference      { typedef T type; }
    
            template <typename T>
            struct remove_reference<T&>  { typedef T type; }
    
            template <typename T>
            struct remove_reference<T&&> { typedef T type; }
    
            template <typename T>
            T&& forward(typename remove_reference<T>::type&& a)
            {
                return static_cast<T&&>(a);
            }
    
            template <typename T>
            T&& forward(typename remove_reference<T>::type& a)
            {
                return static_cast<T&&>(a);
            }
    
            template <typename T>
            struct subscript_impl
            {
                subscript_impl(T&& arg) : arg_(arg) {}
    
                template <typename U>
                auto operator()(U&& u) const ->
                    decltype(detail::declval<U>()[detail::declval<T>()])
                {
                    return u[arg_];
                }
            private:
                mutable T arg_;
            };
        }
    
        #define OPS_DEFINE_BINARY_OP(name, op)                              \
            struct name                                                     \
            {                                                               \
                template <typename T, typename U>                           \
                auto operator()(T&& t, U&& u) const ->                      \
                    decltype(detail::declval<T>() op detail::declval<U>())  \
                {                                                           \
                    return detail::forward<T>(t) op detail::forward<U>(u);  \
                }                                                           \
            }
    
        OPS_DEFINE_BINARY_OP(plus,               +  );
        OPS_DEFINE_BINARY_OP(minus,              -  );
        OPS_DEFINE_BINARY_OP(multiplies,         *  );
        OPS_DEFINE_BINARY_OP(divides,            /  );
        OPS_DEFINE_BINARY_OP(modulus,            %  );
    
        OPS_DEFINE_BINARY_OP(logical_or,         || );
        OPS_DEFINE_BINARY_OP(logical_and,        && );
    
        OPS_DEFINE_BINARY_OP(equal_to,           == );
        OPS_DEFINE_BINARY_OP(not_equal_to,       != );
        OPS_DEFINE_BINARY_OP(less,               <  );
        OPS_DEFINE_BINARY_OP(greater,            >  );
        OPS_DEFINE_BINARY_OP(less_equal,         <= );
        OPS_DEFINE_BINARY_OP(greater_equal,      >= );
    
        OPS_DEFINE_BINARY_OP(bitwise_and,        &  );
        OPS_DEFINE_BINARY_OP(bitwise_or,         |  );
        OPS_DEFINE_BINARY_OP(bitwise_xor,        ^  );
        OPS_DEFINE_BINARY_OP(left_shift,         << );
        OPS_DEFINE_BINARY_OP(right_shift,        >> );
    
        OPS_DEFINE_BINARY_OP(assign,             =  );
        OPS_DEFINE_BINARY_OP(plus_assign,        += );
        OPS_DEFINE_BINARY_OP(minus_assign,       -= );
        OPS_DEFINE_BINARY_OP(multiplies_assign,  *= );
        OPS_DEFINE_BINARY_OP(divides_assign,     /= );
        OPS_DEFINE_BINARY_OP(modulus_assign,     %= );
        OPS_DEFINE_BINARY_OP(bitwise_and_assign, &= );
        OPS_DEFINE_BINARY_OP(bitwise_or_assign,  |= );
        OPS_DEFINE_BINARY_OP(bitwise_xor_assign, ^= );
        OPS_DEFINE_BINARY_OP(left_shift_assign,  <<=);
        OPS_DEFINE_BINARY_OP(right_shift_assign, >>=);
    
        #define OPS_DEFINE_COMMA() ,
        OPS_DEFINE_BINARY_OP(comma, OPS_DEFINE_COMMA());
        #undef OPS_DEFINE_COMMA
    
        #undef OPS_DEFINE_BINARY_OP
    
        #define OPS_DEFINE_UNARY_OP(name, pre_op, post_op)                  \
        struct name                                                         \
        {                                                                   \
            template <typename T>                                           \
            auto operator()(T&& t) const ->                                 \
                decltype(pre_op detail::declval<T>() post_op)               \
            {                                                               \
                return pre_op detail::forward<T>(t) post_op;                \
            }                                                               \
        }
    
        OPS_DEFINE_UNARY_OP(dereference,      * ,   );
        OPS_DEFINE_UNARY_OP(address_of,       & ,   );
        OPS_DEFINE_UNARY_OP(unary_plus,       + ,   );
        OPS_DEFINE_UNARY_OP(logical_not,      ! ,   );
        OPS_DEFINE_UNARY_OP(negate,           - ,   );
        OPS_DEFINE_UNARY_OP(bitwise_not,      ~ ,   );
        OPS_DEFINE_UNARY_OP(prefix_increment, ++,   );
        OPS_DEFINE_UNARY_OP(postfix_increment,  , ++);
        OPS_DEFINE_UNARY_OP(prefix_decrement, --,   );
        OPS_DEFINE_UNARY_OP(postfix_decrement,  , --);
        OPS_DEFINE_UNARY_OP(call,               , ());
        OPS_DEFINE_UNARY_OP(throw_expr,   throw ,   );
        OPS_DEFINE_UNARY_OP(sizeof_expr, sizeof ,   );
    
        #undef OPS_DEFINE_UNARY_OP
    
        template <typename T>
        detail::subscript_impl<T> subscript(T&& arg)
        {
            return detail::subscript_impl<T>(detail::forward<T>(arg));
        }
    
        #define OPS_DEFINE_CAST_OP(name, op)                                \
            template <typename Target>                                      \
            struct name                                                     \
            {                                                               \
                template <typename Source>                                  \
                Target operator()(Source&& source) const                    \
                {                                                           \
                    return op<Target>(source);                              \
                }                                                           \
            }
    
        OPS_DEFINE_CAST_OP(const_cast_to,       const_cast      );
        OPS_DEFINE_CAST_OP(dynamic_cast_to,     dynamic_cast    );
        OPS_DEFINE_CAST_OP(reinterpret_cast_to, reinterpret_cast);
        OPS_DEFINE_CAST_OP(static_cast_to,      static_cast     );
    
        #undef OPS_DEFINE_CAST_OP
    
        template <typename C, typename M, M C::*PointerToMember>
        struct get_data_member
        {
            template <typename T>
            auto operator()(T&& arg) const ->
                decltype(detail::declval<T>().*PointerToMember)
            {
                return arg.*PointerToMember;
            }
        };
    
        template <typename C, typename M, M C::*PointerToMember>
        struct get_data_member_via_pointer
        {
            template <typename T>
            auto operator()(T&& arg) const ->
                decltype(detail::declval<T>()->*PointerToMember)
            {
                return arg->*PointerToMember;
            }
        };
    }
    

    我省略了newdelete(以及它们的各种形式),因为使用它们编写异常安全代码太困难了 :-).

    call实现仅限于nullary operator()重载; 使用可变参数模板,您可能可以将其扩展到支持更广泛的重载范围,但实际上最好使用lambda表达式或库(例如std::bind)来处理更高级的调用场景。对于.*->*实现也是如此。

    剩下的可重载运算符都已提供,甚至包括像sizeofthrow这样的愚蠢的运算符。

    【以上代码是独立的;不需要标准库头文件。我承认在rvalue引用方面还有点小白,所以如果我做错了什么,希望有人能让我知道。】


    我不确定,但我认为你可能也需要在尾返回类型中使用forward。再说一遍,不确定。 - Xeo
    @Xeo:decltype 应该已经根据表达式是 xvalue 还是 lvalue 给出 T&&T&。 @Billy:); 是一个不高兴的脸,不是哈哈大笑的表情。;-) - James McNellis

    6
    可能的原因是大多数开发者不需要它们。其他人使用Boost.Lambda,大多数人都在那里。

    现在,随着C++0x语言的lambda支持,甚至没有更多的理由将它们添加到标准库中。 - Ben Voigt
    1
    @Ben:除了标准的多态之外,所有的lambda都是单态的。也就是说,如果我想写像这个这样的东西,我不能使用lambda,因为我无法在内联中指定事物。 - Billy ONeal
    @Luc:这怎么可能是单态?这是编译时多态。 - Billy ONeal
    @Billy std::plus<T> 不接受 (U, U)(假设没有隐式转换)。只要 u + u 可行,Boost 就可以使用。因此,你不能使用 std::plus<T> 处理例如 std::tuple<int, long*> 元素(警告:示例是严重牵强的,不应视为实际问题解决方案)。 - Luc Danton
    1
    @Billy:你在争论语义学。这就像我说“C++可以在编译时内联函数”,而你说“不,那是运行时...编译器是在运行的”。 - GManNickG
    显示剩余3条评论

    5

    很可能,标准委员会的任何人都没有想到它们会有用。而且随着C++0x的lambda支持,它们中没有一个是有用的。

    编辑:

    我不是说它们没有用-更多的是委员会中没有人想到那个用途。


    2
    它们仍然有用,因为即使您无法更改类型的实现,它们也可以被覆盖以适用于特定类型。 - Mark Ransom
    @Mark:DeadMG并不是说传递一个函数对象来自定义行为没有用处。只是当重新创建它们的代码变得如此简短时,由库编写的函数对象已经不再有太多好处了。 - Ben Voigt
    @Mark Ransom:我也不是说它们一定没有用处——只是委员会上没有人想到那个用途。 - Puppy
    1
    @Mark:虽然这样做有争议,但那真的很丑陋。如果我的类没有实现operator<,你凭什么说std::less应该为其定义良好呢?如果我想让它具有小于比较功能,我会自己实现的。 - GManNickG
    1
    @DeadMG:那么在派生类中定义operator<。 @Mark:由于有模板参数,因此不需要使用std::less来使用自定义排序顺序。 @GMan:您有权为任何您编写的类专门化std::less(具有损坏的实现),从而阻止其使用。但我同意您不应该这样做。 - Ben Voigt
    显示剩余2条评论

    2

    位运算符是在C++0x中添加的。我还发现not_equal_to已经存在。

    其他运算符,如sizeof和一些强制类型转换,是编译时运算符,因此在函数对象中的用处会比较少。

    new和delete被抽象为分配器(allocators)。我们需要更多这种吗?


    我更多地考虑列出它们,而不是哪些特定的使得意义。在 sizeof 的情况下,我同意你的看法,因为它是编译时的,但我可以看到强制转换会很有用。(例如,通过先使用 dynamic_cast 对指针进行向下转型,从而能够调用成员函数上的 std::for_each - Billy ONeal
    你关于 not_equal_to 的观点是正确的 - 已经在问题中进行了修正。因此给你加1分。 - Billy ONeal

    1

    new 没有一个真正的函数对象,但默认分配器只是简单地将请求传递给 new。这也涵盖了 delete,因为它们是相关联的。

    从那里开始,我认为函数对象并不是真正意义上“传递给 for_each 的东西”,而是“你可能需要根据情况进行特殊化的东西”。

    从列表中删除 new [和家族],你基本上有一堆没有实际意义的操作,除非按照语言规定指定。如果你获取一个对象的地址,你真正想要发生的事情只有一件:你会得到该对象的地址。如何可能会改变,但什么不会。所以没有必要通过标准函数对象来专门化这种行为;你可以使用 & 并相信运算符重载会发挥作用。但“添加”或“比较”的含义可能会在程序的过程中发生变化,因此提供一种方法来做这件事是有一定价值的。

    这也包括复合赋值运算符; 它们的含义与它们的两个部分相关联,因此如果您需要 std :: add_assign ,则可以回退到 std :: add [以及不存在于列表中的 operator = ]。

    位运算符有点介于两者之间; 对于它们,我可以看出双方的争论。


    -1
    所有列出来的都是双参数函数对象,但下面并非全部如此。实际上只有`>>`, `<<`, `&`, `|`和`!=`满足这个条件,并且在函数对象方面仅仅略微有用。特别的强制类型转换被视作模板,因此它们的用处稍微小一些。

    3
    这些“casts”不是模板。它们可能与模板共享语法,但实际上它们是操作符。您不会使用“std :: dynamic_cast”。 - Billy ONeal
    3
    std::negatestd::logical_not是一元函数对象。 - Billy ONeal

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