18

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.

Nir Friedman
  • 15,634
  • 2
  • 33
  • 63
  • 1
    why was this downvoted? – Untitled123 Dec 30 '15 at 23:39
  • 1
    Are you sure that with the `friend`version, `assert(v1 == e);` would compile? – YSC Dec 30 '15 at 23:44
  • @YSC Yes, I have added a link to Coliru, let me know if it addresses your question fully. – Nir Friedman Dec 30 '15 at 23:53
  • Ok It does. THank you for the mcve. – YSC Dec 30 '15 at 23:56
  • 1
    Not an answer because this is just a guess, but I believe the definition of vector and its associated functions is older than this particular application of the friend feature. – Sebastian Redl Dec 30 '15 at 23:57
  • That's not an MCVE, as it compiles and runs as expected, it doesn't demonstrate the problem. How is `StrongTypedef` actually defined? In your example even `e.size()` won't compile. – Oktalist Dec 31 '15 at 00:02
  • @SebastianRedl A pretty interesting guess, although honestly I'd be a bit surprised it that were the case. Are you saying that in C++98, the second approach would fail to compile, or have different semantics? – Nir Friedman Dec 31 '15 at 00:02
  • @NirFriedman No, I'm saying that in the evolution of leading up to C++98, the definition of vector was fixed before friend functions defined in the class worked the way they do now, and the committee didn't think of changing vector, or didn't think it was worth the time. – Sebastian Redl Dec 31 '15 at 00:04
  • @SebastianRedl This use of `friend` is presented in Barton & Nackman (1994) _Scientific and Engineering C++_, so pretty much contemporaneous with the standardization of STL – Oktalist Dec 31 '15 at 00:05
  • @Oktalist I edited the coliru link to show what happens with the first approach. I'm not going to get into the entire definition of StrongTypedef, it's way too involved. I don't understand what e.size() has to do with anything, an implicit conversion cannot occur on an object when you are calling its member function. – Nir Friedman Dec 31 '15 at 00:06
  • @NirFriedman The reason I ask is because it works if `StrongTypedef` inherits from `T`, which is how I would implement `StrongTypedef` anyway: http://coliru.stacked-crooked.com/a/52bd27347984cbb4 You even get the implicit conversion for free – Oktalist Dec 31 '15 at 00:10
  • Personally, I'd be less than happy if `v1 == e` converted `e` to a vector (so I wouldn't define an implicit conversion from `EmployeeScores` to a `vector`). If you really want to do such comparisons, provide appropriate overloaded operators for the `EmployeeScores` class - the implementation of those is trivial. – Peter Dec 31 '15 at 00:13
  • @Oktalist: You cannot inherit from `int` though. – Jarod42 Dec 31 '15 at 00:15
  • @Oktalist This is a fair point, but there's a lot more to StrongTypedef than just that, it has traits which can expose or not expose various functionality like comparison, concatenation, etc. This is probably doable as well with inheritance but a different approach will be necessary, and since inheritance doesn't work for primitives (as Jarod noted), I will need to implement and maintain both. As you can see, these traits inject a lot more complexity, which is why I didn't really want to get into it :-). – Nir Friedman Dec 31 '15 at 00:24
  • @Jarod42 I wonder though if it would be possible to work around this by inheriting from a class that can be implicitly converted to and from int. – Nir Friedman Dec 31 '15 at 00:25
  • @Peter The decision to implicitly convert back, as well as the presence of other overloaded operators is controlled by traits. That way one implementation provides typedefs that are suitable in a range of situations. The implementation is trivial, but it's kind of a pain: 6 relational operators, x2 for asymmetry, that's 12 operators. In addition to the 6 that I already provide between the typdef'ed class, that's 18 relational operators. – Nir Friedman Dec 31 '15 at 00:28
  • But, philosophically, isn't the whole point of a `StrongTypedef` class to explicitly exercise control over how variables/types behave and interact? Wanting the compiler to be permissive with implicit type conversions seems at odds with that. Yeah, okay, it may be less convenient but declaring and defining that range of operators is the mechanism by which you control such behaviour. – Peter Dec 31 '15 at 00:44
  • @Peter well, there's obviously two directions of implicit conversion: base type to type def, and vice versa. I'd take the position that allowing implicit base type to type def is flat out wrong. But the reverse conversion is a matter of taste and context; I think your view is entirely valid but don't personally agree it's always correct. – Nir Friedman Dec 31 '15 at 01:01

2 Answers2

2

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.

Yakk - Adam Nevraumont
  • 235,777
  • 25
  • 285
  • 465
  • "As a guess, today Koenig operators would be used in place of template operators." I doubt it. `basic_string_view` added a "sufficient additional overloads" rule instead to handle implicit conversions. – T.C. Dec 31 '15 at 02:02
0

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;
}
Leandro T. C. Melo
  • 3,852
  • 19
  • 22
  • `A` claims to be implicitly convertable-to from `B`. A better version has `B` that has `operator A`, but in both cases invariants being violated mostly mean the conversion code is bad. A bigger concern to me would be cost, but even then expensive implicit conversions are also code smell. – Yakk - Adam Nevraumont Dec 31 '15 at 01:46
  • By giving A a non explicit constructor from B, you have agreed that everything that accepts an A will also accept a B. There's nothing special about the comparison operator here. Also, in terms of the standard, A is taking the role of vector, and vector has no such constructors. – Nir Friedman Dec 31 '15 at 01:47
  • The visibility issue is present, but it can only ever make a difference if you actually call with the syntax operator==(...). So changing this would be backwards incompatible. But nobody really wants to do this, so it doesn't explain why it is like this in the first place. – Nir Friedman Dec 31 '15 at 01:52