透明比较器是什么?

133
在C++14中,关联容器似乎已经从C++11发生了变化 - [associative.reqmts]/13说:
“除非存在类型,否则成员函数模板find、count、lower_bound、upper_bound和equal_range不应参与重载决议。”
使比较器“透明”的目的是什么?
C++14还提供了这样的库模板:
template <class T = void> struct less {
    constexpr bool operator()(const T& x, const T& y) const;
    typedef T first_argument_type;
    typedef T second_argument_type;
    typedef bool result_type;
};

template <> struct less<void> {
    template <class T, class U> auto operator()(T&& t, U&& u) const
    -> decltype(std::forward<T>(t) < std::forward<U>(u));
    typedef *unspecified* is_transparent;
};

例如,std::set<T, std::less<T>>没有透明比较器,但是 std::set<T, std::less<>>一个。
这个改变解决了什么问题?它是否改变了标准容器的运作方式?例如,std::set 的模板参数仍然是 Key, Compare = std::less<Key>, ...,因此默认的 set 是否失去了其 findcount 等成员?

例如,参见此cppreference描述。现在我感到很愚蠢,因为我注意到了“成员函数模板”这个词... - Kerrek SB
5
可能相关:https://dev59.com/1GMk5IYBdhLWcg3w0RGU - user1508519
cppreference网站上也有一个关于http://en.cppreference.com/w/cpp/utility/functional/less_void的简介。 - Cubbi
4个回答

81

这个解决了什么问题,

请参考Dietmar's answerremyabel's answer

这会改变标准容器的工作方式吗?

默认情况下不会。

find等新成员函数模板重载允许您使用可与容器的键进行比较的类型,而不是使用键类型本身。请参见Joaquín Mª López Muñoz的N3465,其中包含详细的理由和一个精心编写的提案来添加此功能。

在布里斯托尔会议上,LWG同意异构查找功能是有用且可取的,但我们无法确定Joaquín的提案在所有情况下都是安全的。N3465提案将对某些程序造成严重问题(请参见现有代码的影响部分)。Joaquín准备了一个更新的草案,其中包含一些具有不同权衡的替代实现,这对帮助LWG了解利弊非常有用,但它们都有破坏某些程序的风险,因此没有共识添加该功能。我们决定,尽管无法无条件地添加该功能,但如果默认禁用并只有“选择加入”,则是安全的。

N3657提案的关键区别(这是我和STL基于N3465和Joaquín的后来未发表的草案的最后一分钟修订)是添加 is_transparent 类型作为可以用于选择新功能的协议。

如果您不使用“透明函数对象”(即定义 is_transparent 类型的函数对象),则容器的行为与以往相同,这仍然是默认值。

如果您选择使用 std :: less <> (这是C ++ 14的新功能)或另一个“透明函数对象”类型,则可以获得新功能。

使用别名模板很容易使用std :: less <>

template<typename T, typename Cmp = std::less<>, typename Alloc = std::allocator<T>>
  using set = std::set<T, Cmp, Alloc>;

名称 is_transparent 来自于STL的N3421,该标准将“钻石操作符”添加到C++14中。 “透明函数对象”是一种接受任何参数类型(不需要相同)并将这些参数简单地转发给另一个运算符的函数对象。这种函数对象恰好是异构查找关联容器所需的内容,因此在所有钻石操作符中添加了类型is_transparent,并将其用作标记类型,以指示应在关联容器中启用新功能。从技术上讲,容器不需要“透明函数对象”,只需要支持使用异构类型调用它(例如,根据STL的定义,https://dev59.com/1GMk5IYBdhLWcg3w0RGU#18940595中的pointer_comp类型不是透明的,但定义pointer_comp::is_transparent允许它用于解决问题)。如果您只使用类型为Tint的键在std::set<T, C>中进行查找,则C只需要能够处理类型为Tint的参数(无论顺序如何),它不需要真正透明。我们选择了这个名称,部分原因是因为我们想不出更好的名称(我更喜欢is_polymorphic,因为这样的函数对象使用静态多态性,但已经有一个std::is_polymorphic类型特征,它指的是动态多态性)。

4
你是那个在woolstar分享的谈话中,被STL告知“当然你可以在脑中执行模板参数推导”的人吗? - Kerrek SB
14
不,我不在那里,但有些人脑中的编译器比我更严格地符合规范 :) - Jonathan Wakely
1
我猜“钻石操作符”指的是链接提案中的<>,但该提案并没有引入<> - 它是用于表示空模板参数列表的现有语法。 “钻石操作符函数对象”可能会稍微清晰一些。 - Qwertie
@JonathanWakely - 你知道为什么std::string没有指定异构比较器吗?(https://dev59.com/zXUOtIcB2Jgan1znwXSj) - Martin Ba
在C++20中,等效功能已添加到unordered_set,因此仅受到相当新的编译器支持,例如GCC 11。 - Jonathan Wakely
显示剩余2条评论

37
在C++11中,没有成员模板find()lower_bound()等。也就是说,这个变化并没有造成任何损失。成员模板是通过n3657引入的,以允许关联容器使用异构键。除了好的和坏的例子外,我没有看到任何具体的有用的例子! is_transparent的作用是避免不必要的转换。如果成员模板没有受到限制,现有的代码可能会直接通过对象,而这些对象在没有成员模板的情况下会被转换。 n3657中的示例用例是使用字符串字面量在std::set<std::string>中定位对象:使用C++11定义时,在将字符串字面量传递给相应的成员函数时会构造std::string对象。通过更改,可以直接使用字符串字面量。如果潜在的比较函数对象完全基于std::string实现,那么就会出问题,因为现在每次比较都会创建一个std::string。另一方面,如果潜在的比较函数对象可以接受std::string和字符串字面量,那么就可以避免构造临时对象。
比较函数对象中的嵌套is_transparent类型提供了一种指定是否应使用模板化成员函数的方法:如果比较函数对象可以处理异构参数,则定义此类型以表示它可以有效地处理不同的参数。例如,新的运算符函数对象只是委托给operator<()并声称是透明的。至少对于具有采用char const*作为参数的重载小于运算符的std::string来说,这是有效的。由于这些函数对象也是新的,即使它们做错了事情(即某些类型需要转换),它们至少不会导致性能降低的静默更改。

谢谢 - 请看我在另一个问题上的评论:你默认获得透明行为吗? - Kerrek SB
8
当在比较函数对象中定义is_transparent时,透明行为才会启用,这是根据23.2.4 [associative.reqmts]第13段的规定。根据23.4.2 [associative.map.syn]和23.4.3 [associative.set.syn]的规定,默认的比较函数对象是std::less <Key>。根据20.10.5 [comparison]第4段的规定,std::less<...>的一般模板不定义名为is_transparent的嵌套类型,但是std::less<void>专业化则定义了该类型。也就是说,默认情况下您没有透明运算符。 - Dietmar Kühl
你有关于命名的任何想法吗?我的意思是为什么要使用 is_transparent 这个名称? - plasmacel
你想要一个“具体的例子来说明这个有用吗?”这里是我的使用案例 - spraff

23
以下全是从 n3657 复制粘贴过来的。

Q. What is the purpose of making an comparator "transparent"?

A. The associative container lookup functions (find, lower_bound, upper_bound, equal_range) only take an argument of key_type, requiring users to construct (either implicitly or explicitly) an object of the key_type to do the lookup. This may be expensive, e.g. constructing a large object to search in a set when the comparator function only looks at one field of the object. There is strong desire among users to be able to search using other types which are comparable with the key_type.

Q. What problem does this solve

A. The LWG had concerns about code like the following:

std::set<std::string> s = /* ... */;
s.find("key");

In C++11 this will construct a single std::string temporary and then compare it with elements to find the key.

With the change proposed by N3465 the std::set::find() function would be an unconstrained template which would pass the const char* through to the comparator function, std::less, which would construct a std::string temporary for every comparison. The LWG considered this performance problem to be a serious issue. The template find() function would also prevent finding NULL in a container of pointers, which causes previously valid code to no longer compile, but this was seen as a less serious issue than the silent performance regression

Q. does this change how standard containers work

A. This proposal modifies the associative containers in and by overloading the lookup member functions with member function templates. There are no language changes.

Q. so does the default set lose its find, count, etc. members

A. Almost all existing C++11 code is unaffected because the member functions are not present unless new C++14 library features are used as the comparison functions.

引用Yakk的话:

在C++14中,如果存在Compare::is_transparent,则std::set::find是一个模板函数。您传递的类型不需要是Key,只需要在您的比较器下等效即可。

以及n3657:

在23.2.4 [associative.reqmts]中添加第13段: 成员函数模板find、lower_bound、upper_bound和equal_range在类型Compare::is_transparent存在时不参与重载决议。

n3421提供了一个"Transparent Operator Functors"的示例。

完整代码在此处


1
std::set<std::string> 是否真正从“通过 char const * 传递”中受益,还是需要创建一个 std::set<std::string, std::less<>> - Kerrek SB
@Kerrek 我认为他们试图避免的问题是“传递char const *”,如果我没有理解错的话。看一下措辞:“通过N3465提出的更改,std::set :: find()函数将成为一个无约束模板,它将通过比较器函数std :: less <std :: string>将const char *传递给临时构造std :: string进行每次比较。 LWG认为这个性能问题是一个严重的问题。 - user1508519
你的报价和我在第13段的说法相反:“除非类型存在/不存在”...?! - Kerrek SB
@Kerrek,我复制粘贴了,所以我不知道那是关于什么的。 - user1508519
5
@KerrekSB,那是我的错误,N3657应该说“存在”,但我写成了“不存在”……那是在最后一刻写的一篇晚期论文。草案标准是正确的。 - Jonathan Wakely
4
是的,引用我原本想说的话可能会更清晰些,而不是当时实际说的话 :) - Jonathan Wakely

7
Stephan T Lavavej谈到了编译器不断创建临时变量的问题,以及他在C++1y中提出的透明运算符函数对象的解决方案。请参考以下链接:GoingNative 2013 - Dont help the Compiler(大约在一小时的时候)。

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