Remember the ifstream
ifstream
Quick, what does this C++17 code print?
void foo(const std::istream&) {
puts("istream");
}
void foo(const std::ifstream&) {
puts("ifstream");
}
int main() {
std::fstream t;
foo(t);
}
(Wandbox.) That’s right, it prints “istream” —
the fstream
class (input/output, file-based) is derived from istream
(input, non-file)
and also from ostream
(output, non-file) but not from ifstream
(input, file-based) and
not from ofstream
(output, file-based).
There is no inheritance relationship
between fstream
, ifstream
, and ofstream
. But there is an inheritance relationship between
fstream
and iostream
. The spaghetti of C++98-era multiple inheritance looks like this:
Quick, what does this C++2a Working Draft code print?
void foo(Same<int> auto) {
puts("exactly int");
}
void foo(Integral auto) {
puts("integral");
}
void foo(Regular auto) {
puts("regular");
}
int main() {
int t = 42;
foo(t);
}
Okay, trick question. This code doesn’t compile: out of Same<int>
, Integral
, and Regular
, none
of them subsumes any of the others, so the overload resolution is ambiguous. (This is not a bad thing,
in this case! Nobody should be writing code like the above.)
The spaghetti of C++2a-era multiple subsumption looks like this:
Just like an old-time cartographer, I have filled in the blank space on the west side of my map
with non-sequitur dragons; in this case RegularInvocable
.
(That’s right, there is no relationship between Regular
and RegularInvocable
— the C++2a Ranges
proposal introduced the term Regular
to the standard library and immediately overloaded it with
multiple unrelated meanings.)
To be fair, I haven’t come up with an easy surprising example such as my ifstream
example
above. The best I can do is stuff like
-
Same<T, int>
does not subsumeIntegral<T>
-
Same<T, int>
does subsumeSame<int, T>
(!), and vice versa -
Same<T, Base>
does not subsumeDerivedFrom<T, Base>
-
SwappableWith<T, T>
does not subsumeSwappable<T>
, nor vice versa
Concepts are a lot weirder than classical inheritance, because they’re parameterized. As you can see
above, Constructible<T>
is a different concept (with different “derived concepts”)
from Constructible<T, T>
. And so we can get extra weirdnesses such as
DerivedFrom<T, std::ifstream>
does not subsumeDerivedFrom<T, std::istream>
So in conclusion: Remember the ifstream! Resist designing libraries with many couplings; resist implementing couplings on an ad-hoc basis. Today’s cutesy “because-we-can” spaghetti graph is tomorrow’s “but-why-would-you?” spaghetti graph.