
I look at the problem like this: if you don't assign types, then the question is, for which set of types of a and b can the expression a == b define an equivalence relation? I know that the behavior is correct for char and int or string and const char*. Why? What's special about those types that make them behave as if I was comparing objects of a single type?
They are special because we give them higher level semantics which define what that equivalence relation is and under what conditions it holds.
Yes, and that mapping of semantics can be generalized in a consistent and meaningful way.
We tend to be imprecise with (or just plain leave out) those preconditions. Take you example of string and const char*; the following asserts will either sometimes fire or invoke undefined behavior for a given std::string s, const char* p, or string literal l:
assert(s == s.c_str()); assert(std::string(p) == p); assert(std::string(l, sizeof(l) - 1) == l);
Now, I can easily state the preconditions, but those requirements are no longer on the types themselves but upon the values they represent.
Sure, there are always limitations. Do those limitations or exceptions imply that strings aren't equal to strings, char*'s or string literals? There's a difference between preconditions and the semantics of e.g. an equivalence relation. The specification of the equivalence relation is inherently (sometimes explicitly) defined on well-formed values. Preconditions limit the set of values or expressions that are not well-formed.