¿Por qué std :: set no tiene una función miembro "contiene"?

103

Lo uso mucho std::set<int>y, a menudo, simplemente necesito verificar si dicho conjunto contiene un número o no.

Me resultaría natural escribir:

if (myset.contains(number))
   ...

Pero debido a la falta de un containsmiembro, necesito escribir lo engorroso:

if (myset.find(number) != myset.end())
  ..

o lo no tan obvio:

if (myset.count(element) > 0) 
  ..

¿Hay alguna razón para esta decisión de diseño?

Jabberwocky
fuente
7
La mayoría de la biblioteca estándar funciona con iteradores, por lo que normalmente las funciones que devuelven iteradores son lo que cabría esperar. Sin embargo, no es difícil escribir una función para abstraer eso. Lo más probable es que el compilador lo inserte, ya que solo debería ser una línea o 2 de código y obtendrá el mismo rendimiento.
NathanOliver
3
Otro problema (más fundamental) con el count()enfoque es que hace más trabajo del que countains()tendría que hacer.
Leo Heinsaar
11
La razón fundamental detrás de esa decisión de diseño es que contains()el que devuelve una boolsería perder información valiosa sobre los que el elemento está en la colección . find()conserva y devuelve esa información en forma de iterador, por lo que es una mejor opción para una biblioteca genérica como STL. (Sin bool contains()embargo, eso no quiere decir que no sea muy agradable de tener o incluso necesario)
Leo Heinsaar
3
Es fácil escribir una contains(set, element)función gratuita utilizando la interfaz pública del conjunto. Por tanto, la interfaz del conjunto es funcionalmente completa; agregar un método de conveniencia solo aumenta la interfaz sin habilitar ninguna función adicional, que no es la forma de C ++.
Toby Speight
3
¿Cerramos todo estos días? ¿Cómo es esta pregunta "principalmente basada en opiniones" de alguna manera?
Mr. Alien

Respuestas:

148

Creo que probablemente fue porque estaban tratando de hacer std::sety lo std::multisetmás similar posible. (Y obviamente counttiene un significado perfectamente sensato para std::multiset).

Personalmente creo que esto fue un error.

No se ve tan mal si finge que countes solo un error ortográfico containsy escribe la prueba como:

if (myset.count(element)) 
   ...

Sin embargo, sigue siendo una pena.

Martin Bonner apoya a Monica
fuente
5
Por cierto, es exactamente lo mismo con mapas y multimapas (que es tan feo, pero menos feo que todas estas comparaciones con .end()).
Matteo Italia
8
Alternativamente, es posible que no hayan visto la necesidad de un miembro adicional contains(), debido a que sería redundante porque para cualquier std::set<T> sy T t, el resultado de s.contains(t)es exactamente idéntico al resultado de static_cast<bool>(s.count(t)). Como el uso del valor en una expresión condicional lo convertiría implícitamente en bool, es posible que hayan sentido que count()sirvió bastante bien para el propósito.
Justin Time - Reincorpora a Monica
2
¿Error de ortografía? if (myset.ICanHaz(element)) ...: D
Stéphane Gourichon
3
@MartinBonner Realmente no importa si las razones para dejarlo fuera fueron tontas. Tampoco importa si la conversación no fue el 100% del fundamento final. Su respuesta aquí es solo una suposición razonable de cómo cree que debería ser. La conversación y la respuesta de alguien no solo involucrado en ella, sino que tiene la tarea de proponerla (aunque no lo hicieron) está indiscutiblemente más cerca de la verdad que esta suposición, sin importar cómo se mire. Como mínimo, al menos debe mencionarlo en esta respuesta, eso sería una gran mejora y sería lo más responsable a hacer.
Jason C
2
@JasonC: ¿Podrías seguir adelante y editar una sección en la parte inferior, por favor? Realmente no entiendo el punto que está tratando de hacer, y los comentarios probablemente no sean la mejor manera de aclararlo. ¡Gracias!
Martin Bonner apoya a Monica
44

Para poder escribir if (s.contains()), contains()tiene que devolver un bool(o un tipo convertible a bool, que es otra historia), como binary_searchhace.

La razón fundamental detrás de la decisión de diseño de no hacerlo de esta manera es que, si se contains()devuelve, boolse perdería información valiosa sobre dónde se encuentra el elemento en la colección . find()conserva y devuelve esa información en forma de iterador, por lo que es una mejor opción para una biblioteca genérica como STL. Este ha sido siempre el principio rector de Alex Stepanov, como ha explicado a menudo (por ejemplo, aquí ).

En cuanto al count()enfoque en general, aunque a menudo es una buena solución, el problema es que hace más trabajo del que contains() tendría que hacer .

Eso no quiere decir que bool contains()no sea muy agradable tener o incluso necesario. Hace un tiempo tuvimos una larga discusión sobre este mismo tema en el grupo ISO C ++ Standard - Future Proposals.

Leo Heinsaar
fuente
5
Y es interesante notar que esa discusión terminó con casi un consenso sobre lo deseable y se le pidió que escribiera una propuesta al respecto.
PJTraill
@PJTraill True, y la razón por la que no seguí adelante es que contains(), obviamente, interactuaría fuertemente con los contenedores y algoritmos existentes, que se verían fuertemente afectados por conceptos y rangos, en el momento que se esperaba que entraran en C ++ 17, y Estaba convencido (como resultado de la discusión y de un par de intercambios de correos electrónicos privados) de que era una mejor idea esperarlos primero. Por supuesto, en 2015 no estaba claro que ni los conceptos ni los rangos no llegarían a C ++ 17 (de hecho, había muchas esperanzas de que lo hicieran). Sin embargo, no estoy seguro de que valga la pena seguirlo ahora.
Leo Heinsaar
1
Porque std::set(que es sobre lo que pregunta la pregunta), no veo cómo countfunciona más de containslo que tendría que hacer. La implementación glibc de countes (aproximadamente) return find(value) == end() ? 0 : 1;. Aparte de los detalles sobre el operador ternario frente al regreso != end()(que esperaría que el optimizador elimine), no veo cómo hay más trabajo.
Martin Bonner apoya a Monica
4
"... contiene () que devuelve un bool perdería información valiosa sobre dónde está el elemento en la colección " - Si el usuario llama myset.contains()(si existiera), sería una indicación bastante fuerte de que esa información no es valiosa ( al usuario en ese contexto).
Keith Thompson
1
¿Por qué hace count()más trabajo del que contains()debería hacer std::set? Es único, por lo que count()podría ser return contains(x) ? 1 : 0;exactamente lo mismo.
Timmmm
22

Le falta porque nadie lo agregó. Nadie lo agregó porque los contenedores del STL que stdincorporó la biblioteca estaban diseñados para ser mínimos en la interfaz. (Tenga en cuenta que std::stringno provino de la STL de la misma manera).

Si no le importa una sintaxis extraña, puede fingirla:

template<class K>
struct contains_t {
  K&& k;
  template<class C>
  friend bool operator->*( C&& c, contains_t&& ) {
    auto range = std::forward<C>(c).equal_range(std::forward<K>(k));
    return range.first != range.second;
    // faster than:
    // return std::forward<C>(c).count( std::forward<K>(k) ) != 0;
    // for multi-meows with lots of duplicates
  }
};
template<class K>
containts_t<K> contains( K&& k ) {
  return {std::forward<K>(k)};
}

utilizar:

if (some_set->*contains(some_element)) {
}

Básicamente, puede escribir métodos de extensión para la mayoría de los stdtipos de C ++ utilizando esta técnica.

Tiene mucho más sentido hacer esto:

if (some_set.count(some_element)) {
}

pero me divierte el método del método de extensión.

Lo realmente triste es que escribir un eficiente containspodría ser más rápido en un multimapo multiset, ya que solo tienen que encontrar un elemento, mientras que counttienen que encontrar cada uno de ellos y contarlos .

Un multiset que contenga mil millones de copias de 7 (ya sabes, en caso de que te quedes sin) puede tener una muy lenta .count(7), pero podría tener una muy rápida contains(7).

Con el método de extensión anterior, podríamos hacerlo más rápido para este caso usando lower_bound, comparando endy luego comparando con el elemento. Sin embargo, hacer eso para un maullido desordenado y ordenado requeriría un SFINAE elegante o sobrecargas específicas del contenedor.

Yakk - Adam Nevraumont
fuente
2
Mil millones de copias de 7? Y aquí pensé std::setque no puede contener duplicados y, por std::set::countlo tanto , siempre volveré 0o 1.
nwp
5
@nwp std::multiset::countcan
milleniumbug
2
@nwp Mi falta de backticksalrededor de la palabra "conjunto" se debe a que no me refiero std::setespecíficamente. Para que se sienta mejor,
agregaré
3
Parece que me estoy perdiendo la broma de lo que se suponía que era una referencia a "miau".
user2357112 apoya a Monica
2
@ user2357112 miau es un marcador de posición para "conjunto o mapa". Pregúntele al STL por qué.
Yakk - Adam Nevraumont
12

Estás mirando un caso particular y no ves un panorama más amplio. Como se indica en la documentación, std::set cumple con los requisitos del concepto AssociativeContainer . Para ese concepto no tiene ningún sentido tener un containsmétodo, ya que es bastante inútil para std::multisety std::multimap, pero countfunciona bien para todos ellos. Aunque el método containspodría añadirse como un alias para countpara std::set, std::mapy sus versiones hash (como lengthpor size()en std::string), pero se parece a los creadores de la biblioteca no vio necesidad real para ello.

Slava
fuente
8
Tenga en cuenta que stringes un monstruo: existía antes del STL, donde tenía lengthy todos esos métodos que están basados ​​en índices, y luego se "contenedor" para encajar dentro del modelo STL ... sin eliminar los métodos existentes por razones de compatibilidad con versiones anteriores . Ver GotW # 84: Monoliths Unstrung => stringrealmente viola el principio de diseño de "cantidad mínima de funciones miembro".
Matthieu M.
5
Pero luego la pregunta es "¿Por qué vale la pena tener un concepto de Contenedor Asociativo como ese?" - y no estoy seguro de que fuera en retrospectiva.
Martin Bonner apoya a Monica
24
Preguntar si un conjunto múltiple, un mapa múltiple o un mapa contiene algo tiene mucho sentido para mí. De hecho, containses igual en esfuerzo en un set / mapa, pero se puede hacer más rápido que counten un multiset / multimap.
Yakk - Adam Nevraumont
5
AssociativeContainer no requiere que las clases no tengan un containsmétodo.
user2357112 apoya a Monica
6
@Slava Eso es como decir size()y empty()son duplicados, pero muchos contenedores tienen ambos.
Barry
10

Aunque no sé por qué std::setno tiene containspero countque solo regresa 0o 1, puede escribir una containsfunción de ayuda con plantilla como esta:

template<class Container, class T>
auto contains(const Container& v, const T& x)
-> decltype(v.find(x) != v.end())
{
    return v.find(x) != v.end();
}

Y utilícelo así:

    if (contains(myset, element)) ...
rustyx
fuente
3
-1, porque esto se contradice directamente por el hecho de que, de hecho, el containsmétodo existe, simplemente se nombra de una manera estúpida.
Matteo Italia
4
"El STL se esfuerza por ofrecer una interfaz mínima" chough std::string cough
bolov
6
@bolov: ¿tu punto? std.::string¡NO es parte de STL! Es parte de la biblioteca estándar y se diseñó retroactivamente ...
MFH
3
@MatteoItalia countpuede ser más lento ya que efectivamente necesita hacer dos finds para obtener el comienzo y el final del rango si se comparte el código con multiset.
Mark Ransom
2
El OP ya sabe que es redundante, pero aparentemente quiere que el código se lea explícitamente contains. No veo nada malo con eso. @MarkRansom, el pequeño SFINAE es para evitar que esta plantilla se vincule a cosas que no debería.
rustyx
7

La verdadera razón setes un misterio para mí, pero una posible explicación para este mismo diseño mappodría ser evitar que las personas escriban código ineficiente por accidente:

if (myMap.contains("Meaning of universe"))
{
    myMap["Meaning of universe"] = 42;
}

Lo que resultaría en dos mapbúsquedas.

En cambio, se ve obligado a obtener un iterador. Esto le da una pista mental de que debe reutilizar el iterador:

auto position = myMap.find("Meaning of universe");
if (position != myMap.cend())
{
    position->second = 42;
}

que consume solo una mapbúsqueda.

Cuando nos damos cuenta de eso sety mapestamos hechos de la misma carne, podemos aplicar este principio también a set. Es decir, si queremos actuar sobre un elemento setsolo si está presente en el set, este diseño puede impedirnos escribir código como este:

struct Dog
{
    std::string name;
    void bark();
}

operator <(Dog left, Dog right)
{
    return left.name < right.name;
}

std::set<Dog> dogs;
...
if (dogs.contain("Husky"))
{
    dogs.find("Husky")->bark();
}

Por supuesto, todo esto es una mera especulación.

Martin Drozdik
fuente
1
Sí, pero para los conjuntos de entradas, esto no se aplica.
Jabberwocky
7
Excepto que la gente puede escribir if (myMap.count("Meaning of universe"))muy bien, entonces ...?
Barry
@MichaelWalz Vaya, tienes razón. Modifiqué mi respuesta para incluir también el ejemplo establecido. Sin embargo, el razonamiento de un conjunto de ints es un misterio para mí.
Martin Drozdik
2
Esto no puede ser correcto. Pueden escribir su código ineficiente con la misma facilidad containsque con count.
Martin Bonner apoya a Monica
2

Desde c ++ 20,

bool contains( const Key& key ) const

está disponible.

Andy
fuente
0

¿Qué pasa con binary_search?

 set <int> set1;
 set1.insert(10);
 set1.insert(40);
 set1.insert(30);
 if(std::binary_search(set1.begin(),set1.end(),30))
     bool found=true;
Massimiliano Di Cavio
fuente
No funcionaría std::unordered_set, pero std::setsí lo haría.
Jabberwocky
Es normal, binary_search funciona solo para árboles binarios.
Massimiliano Di Cavio
0

contiene () tiene que devolver un bool. Usando el compilador C ++ 20 obtengo el siguiente resultado para el código:

#include<iostream>
#include<map>
using namespace std;

int main()
{
    multimap<char,int>mulmap;
    mulmap.insert(make_pair('a', 1)); //multiple similar key
    mulmap.insert(make_pair('a', 2)); //multiple similar key
    mulmap.insert(make_pair('a', 3)); //multiple similar key
    mulmap.insert(make_pair('b', 3));
    mulmap.insert({'a',4});
    mulmap.insert(pair<char,int>('a', 4));
    
    cout<<mulmap.contains('c')<<endl;  //Output:0 as it doesn't exist
    cout<<mulmap.contains('b')<<endl;  //Output:1 as it exist
}
bashar
fuente
-1

Otra razón es que le daría al programador la falsa impresión de que std :: set es un conjunto en el sentido de la teoría matemática de conjuntos. Si implementan eso, seguirían muchas otras preguntas: si un std :: set tiene contiene () para un valor, ¿por qué no lo tiene para otro conjunto? ¿Dónde están union (), intersection () y otras operaciones de conjunto y predicados?

La respuesta es, por supuesto, que algunas de las operaciones de conjunto ya están implementadas como funciones en (std :: set_union (), etc.) y otras están implementadas de manera tan trivial como contains (). Las funciones y los objetos de función funcionan mejor con abstracciones matemáticas que los miembros de objeto, y no se limitan al tipo de contenedor en particular.

Si uno necesita implementar una funcionalidad completa de un conjunto matemático, no solo tiene la opción de un contenedor subyacente, sino que también tiene una opción de detalles de implementación, por ejemplo, ¿funcionaría su función theory_union () con objetos inmutables, más adecuada para la programación funcional? , ¿o modificaría sus operandos y ahorraría memoria? ¿Se implementaría como objeto de función desde el principio o sería mejor implementar una función C y usar std :: function <> si es necesario?

Como está ahora, std :: set es solo un contenedor, muy adecuado para la implementación de set en sentido matemático, pero está tan lejos de ser un conjunto teórico como std :: vector de ser un vector teórico.

Mike Tyukanov
fuente