Nir Friedman
Nir Friedman

Reputation: 17704

Why are std::vector and std::string's comparison operators defined as template functions?

A little overview. I'm writing a class template that provides a strong typedef; by strong typedef I am contrasting with a regular typedef which just declares an alias. To give an idea:

using EmployeeId = StrongTypedef<int>;

Now, there are different schools of thought on strong typedefs and implicit conversions. One of these schools says: not every integer is an EmployeeId, but every EmployeeId is an integer, so you should allow implicit conversions from EmployeeId to integer. And you can implement this, and write things like:

EmployeeId x(4);
assert(x == 4);

This works because x gets implicitly converted to an integer, and then integer equality comparison is used. So far, so good. Now, I want to do this with a vector of integers:

using EmployeeScores = StrongTypedef<std::vector<int>>;

So I can do things like this:

std::vector<int> v1{1,2};
EmployeeScores e(v1);
std::vector<int> v2(e); // implicit conversion
assert(v1 == v2);

But I still can't do this:

assert(v1 == e);

The reason this doesn't work is because of how std::vector defines its equality check, basically (modulo standardese):

template <class T, class A>
bool operator==(const vector<T,A> & v1, const vector<T,A> & v2) {
...
}

This is a function template; because it gets discarded in an earlier phase of lookup it will not allow a type that converts implicitly to vector to be compared.

A different way to define equality would be like this:

template <class T, class A = std::allocator<T>>
class vector {
... // body

  friend bool operator==(const vector & v1, const vector & v2) {
  ...
}

} // end of class vector

In this second case, the equality operator is not a function template, it's just a regular function that's generated along with the class, similar to a member function. This is an unusual case enabled by the friend keyword.

The question (sorry the background was so long), is why doesn't std::vector use the second form instead of the first? This makes vector behave more like primitive types, and as you can clearly see it helps with my use case. This behavior is even more surprising with string since it's easy to forget that string is just a typedef of a class template.

Two things I've considered: first, some may think that because the friend function gets generated with the class, this will cause a hard failure if the contained type of the vector does not support equality comparison. This is not the case; like member functions of template classes, they aren't generated if unused. Second, in the more general case, free functions have the advantage that they don't need to be defined in the same header as the class, which can have advantages. But this clearly isn't utilized here.

So, what gives? Is there a good reason for this, or was it just a sub-optimal choice?

Edit: I wrote a quick example that demonstrates two things: both that implicit conversion works as desired with the friend approach, and that no hard failures are caused if the templated type doesn't meet the requirements of the equality operator (obviously, assuming the equality operator is not used in that case). Edit: improved to contrast with the first approach: http://coliru.stacked-crooked.com/a/6f8910945f4ed346.

Upvotes: 21

Views: 1053

Answers (2)

Leandro T. C. Melo
Leandro T. C. Melo

Reputation: 4032

EDIT: After I re-read my explanation and influenced by a few comments around, I'm convinced my original reasoning is not compelling indeed. My answer essentially attempted to argue that although a value x could be implicitly converted to a value y of a different type, an "automagically" equality comparison between the two might not necessarily be expected. For contextualization, I'm still leaving here the code I used as an example.

struct B {};

template <class T>
struct A {
  A() {}
  A(B) {}
  friend bool operator==(const A<T>&, const A<T>&) { return false; }
};

// The template version wouldn't allow this to happen.
// template <class T>
// bool operator==(const A<T>&, const A<T>&) { return false; }

int main() {
  A<B> x;
  B y;
  if (x == y) {} //compiles fine
  return 0;
}

Upvotes: 0

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275670

The techique you describe (what I call Koenig operators) was not known, at least not widely, at the point vector was designed and originally specified.

Changing it now would require more care than using it originally, and more justification.

As a guess, today Koenig operators would be used in place of template operators.

Upvotes: 2

Related Questions