c++ - Uso idiomático de std:: rel_ops
c++-standard-library idioms (4)
¿Cuál es el método preferido para usar std::rel_ops
para agregar el conjunto completo de operadores relacionales a una clase?
This documentación sugiere using namespace std::rel_ops
, pero esto parece tener fallas profundas, ya que significa que incluir el encabezado para la clase implementada de esta manera también agregaría operadores relacionales completos a todas las demás clases con un operator<
definido operator<
y operator==
, incluso si eso no fue deseado. Esto tiene el potencial de cambiar el significado del código de maneras sorprendentes.
Como nota al margen: he estado usando Boost.Operators para hacer esto, pero todavía tengo curiosidad acerca de la biblioteca estándar.
Creo que la técnica preferida no es usar std::rel_ops
en absoluto. La técnica utilizada en boost::operator
( link ) parece ser la solución habitual.
Ejemplo:
#include "boost/operators.hpp"
class SomeClass : private boost::equivalent<SomeClass>, boost::totally_ordered<SomeClass>
{
public:
bool operator<(const SomeClass &rhs) const
{
return someNumber < rhs.someNumber;
}
private:
int someNumber;
};
int main()
{
SomeClass a, b;
a < b;
a > b;
a <= b;
a >= b;
a == b;
a != b;
}
El problema al agregar el espacio de nombres rel_ops, independientemente de si lo hace con un manual que using namespace rel_ops;
o si lo hace automáticamente como se describe en la respuesta de @ bames53 es que agregar el espacio de nombres puede tener efectos secundarios imprevistos en partes de su código. Lo encontré hace poco, ya que había estado usando la solución @bames53 por algún tiempo, pero cuando cambié una de mis operaciones basadas en contenedor para usar un reverse_iterator en lugar de un iterador (dentro de un multimap, pero sospecho que sería el mismo para cualquiera de los contenedores estándar), de repente recibí errores de compilación al usar! = para comparar dos iteradores. Finalmente, lo rastreé hasta el hecho de que el código incluía el espacio de nombres rel_ops que estaba interfiriendo con la definición de reverse_iterators.
Utilizar boost sería una forma de resolverlo, pero como menciona @Tom, no todos están dispuestos a usar boost, incluido yo mismo. Así que implementé mi propia clase para resolver el problema, que sospecho es también cómo lo hace el impulso, pero no revisé las bibliotecas de impulso para ver.
Específicamente, tengo la siguiente estructura definida:
template <class T>
struct add_rel_ops {
inline bool operator!=(const T& t) const noexcept {
const T* self = static_cast<const T*>(this);
return !(*self == t);
}
inline bool operator<=(const T& t) const noexcept {
const T* self = static_cast<const T*>(this);
return (*self < t || *self == t);
}
inline bool operator>(const T& t) const noexcept {
const T* self = static_cast<const T*>(this);
return (!(*self == t) && !(*self < t));
}
inline bool operator>=(const T& t) const noexcept {
const T* self = static_cast<const T*>(this);
return !(*self < t);
}
};
Para usar esto, cuando defines tu clase, di MyClass, puedes heredar de ésta para agregar los operadores "faltantes". Por supuesto, necesita definir los operadores == y <dentro de MyClass (no se muestra a continuación).
class MyClass : public add_rel_ops<MyClass> {
...stuff...
};
Es importante que incluya MyClass
como el argumento de la plantilla. Si MyOtherClass
que incluir una clase diferente, digamos MyOtherClass
, es casi seguro que la static_cast
le static_cast
problemas.
Tenga en cuenta que mi solución supone que los operadores ==
y <
se definen como const noexcept
que es uno de los requisitos de mis estándares de codificación personal. Si sus estándares son diferentes, deberá modificar add_rel_ops en consecuencia.
Además, si le molesta el uso de static_cast
, puede cambiarlos para que sean un dynamic_cast
agregando
virtual ~add_rel_ops() noexcept = default;
a la clase add_rel_ops para convertirla en una clase virtual. Por supuesto, eso también forzará a MyClass
a ser una clase virtual y es por eso que no tomo ese enfoque.
La forma en que el operador sobrecarga las clases definidas por el usuario tenía que funcionar a través de la búsqueda dependiente de los argumentos. ADL permite que los programas y bibliotecas eviten saturar el espacio de nombres global con sobrecargas de operadores, pero aún permite el uso conveniente de los operadores; Es decir, sin calificación explícita del espacio de nombres, que no es posible hacer con la sintaxis del operador infijo a + b
y que en su lugar requeriría la sintaxis de la función normal your_namespace::operator+ (a, b)
.
ADL, sin embargo, no solo busca en todas partes cualquier posible sobrecarga del operador. ADL está restringido a mirar solo las clases ''asociadas'' y los espacios de nombres. El problema con std::rel_ops
es que, como se especifica, este espacio de nombres nunca puede ser un espacio de nombres asociado de ninguna clase definida fuera de la biblioteca estándar y, por lo tanto, ADL no puede funcionar con dichos tipos definidos por el usuario.
Sin embargo, si está dispuesto a hacer trampa, puede hacer que std::rel_ops
funcione.
Los espacios de nombres asociados se definen en C ++ 11 3.4.2 [basic.lookup.argdep] / 2. Para nuestros propósitos, el hecho importante es que el espacio de nombres del que una clase base es miembro es un espacio de nombres asociado de la clase heredada, y por lo tanto, ADL comprobará esos espacios de nombres para las funciones apropiadas.
Entonces, si lo siguiente:
#include <utility> // rel_ops
namespace std { namespace rel_ops { struct make_rel_ops_work {}; } }
fueron (de alguna manera) encontrar su camino en una unidad de traducción, luego en las implementaciones compatibles (ver la siguiente sección), entonces podría definir sus propios tipos de clase, así:
namespace N {
// inherit from make_rel_ops_work so that std::rel_ops is an associated namespace for ADL
struct S : private std::rel_ops::make_rel_ops_work {};
bool operator== (S const &lhs, S const &rhs) { return true; }
bool operator< (S const &lhs, S const &rhs) { return false; }
}
Y entonces ADL funcionaría para su tipo de clase y encontraría los operadores en std::rel_ops
.
#include "S.h"
#include <functional> // greater
int main()
{
N::S a, b;
a >= b; // okay
std::greater<N::s>()(a, b); // okay
}
Por supuesto, agregar make_rel_ops_work
usted mismo hace que el programa tenga un comportamiento indefinido porque C ++ no permite que los programas de usuario agreguen declaraciones a std
. Como ejemplo de cómo eso realmente importa y por qué, si lo hace, puede tomarse la molestia de verificar que su implementación realmente funcione correctamente con esta adición, considere:
Arriba, muestro una declaración de make_rel_ops_work
que sigue a #include <utility>
. Uno podría ingenuamente esperar que incluir esto aquí no importe y que mientras el encabezado esté incluido en algún momento antes del uso de las sobrecargas del operador, entonces ADL funcionará. La especificación, por supuesto, no ofrece esa garantía y existen implementaciones reales donde ese no es el caso.
clang con libc ++, debido al uso de libc ++ de espacios de nombres en línea, (IIUC) considerará que la declaración de make_rel_ops_work
está en un espacio de nombre distinto del espacio de nombres que contiene las sobrecargas de operador <utility>
menos que la declaración de std::rel_ops
<utility>
primero. Esto se debe a que, técnicamente, std::__1::rel_ops
y std::rel_ops
son espacios de nombres distintos, incluso si std::__1
es un espacio de nombres en línea. Pero si clang ve que la declaración del espacio de nombres original para rel_ops
está en un espacio de nombres en línea __1
, tratará un namespace std { namespace rel_ops {
declaration como extendiendo std::__1::rel_ops
lugar de como un nuevo espacio de nombres.
Creo que este comportamiento de extensión del espacio de nombres es una extensión clang en lugar de ser especificado por C ++, por lo que es posible que ni siquiera pueda confiar en esto en otras implementaciones. En particular, gcc no se comporta de esta manera, pero afortunadamente libstdc ++ no usa espacios de nombres en línea. Si no quiere confiar en esta extensión, entonces para clang / libc ++ puede escribir:
#include <__config>
_LIBCPP_BEGIN_NAMESPACE_STD
namespace rel_ops { struct make_rel_ops_work {}; }
_LIBCPP_END_NAMESPACE_STD
pero obviamente necesitará implementaciones para otras bibliotecas que use. Mi declaración más simple de make_rel_ops_work
funciona para clang3.2 / libc ++, gcc4.7.3 / libstdc ++ y VS2012.
No es el mejor, pero puede usar el using namespace std::rel_ops
como detalle de implementación para implementar los operadores de comparación en su tipo. Por ejemplo:
template <typename T>
struct MyType
{
T value;
friend bool operator<(MyType const& lhs, MyType const& rhs)
{
// The type must define `operator<`; std::rel_ops doesn''t do that
return lhs.value < rhs.value;
}
friend bool operator<=(MyType const& lhs, MyType const& rhs)
{
using namespace std::rel_ops;
return lhs.value <= rhs.value;
}
// ... all the other comparison operators
};
Al usar using namespace std::rel_ops;
, permitimos a ADL buscar el operator<=
si está definido para el tipo, pero recurrir a lo definido en std::rel_ops
caso contrario.
Sin embargo, esto todavía es un dolor, ya que todavía tiene que escribir una función para cada uno de los operadores de comparación.