operator auto
operator autoIn a previous post on parameter-only types,
I mentioned in passing the idea of operator auto. For the past couple of years,
when I’ve used that phrase, I’ve meant something like what’s proposed in
Joël Falcou, Peter Gottschling, and Herb Sutter’s P0672 “Implicit Evaluation
of auto Variables” (2017).
That is, we have some class with a bunch of operators that return
expression templates,
or perhaps just proxies (like vector<bool>::iterator). Anyway, they return
objects of some ugly temporary type, probably containing dangling references.
template<class Left, class Right, other, stuff>
class ExpressionTemplate {
Left *left; Right *right;
other stuff;
};
class Matrix {
Matrix(ExpressionTemplate&&);
friend ExpressionTemplate<some, stuff>
operator+(const Matrix&, const Matrix&);
friend ExpressionTemplate<different, stuff>
operator+(const Matrix&, ExpressionTemplate<some, stuff>);
friend ExpressionTemplate<different, stuff>
operator+(ExpressionTemplate<some, stuff>, const Matrix&);
};
So now when we write
Matrix a, b;
Matrix c = a + b + Matrix(1,0, 0,1);
it temporarily generates a bunch of intermediate objects
of type ExpressionTemplate<some, complicated, stuff>, each
of which contains pointers to other objects in the expression.
The top-level ExpressionTemplate object contains a left pointer
to the temporary ExpressionTemplate object representing a + b,
and a right pointer to our temporary (rvalue) Matrix object.
But then that ExpressionTemplate is implicitly converted back into a
Matrix, which does the math and reifies the result with no more
pointers, and we’re all good.
But if we accidentally write
auto c = a + b + Matrix(1,0, 0,1);
this is suddenly not fine! Now the type of c is ExpressionTemplate,
and it contains pointers to temporary objects whose lifetime has
ended — they’re gone! Now when we try to use c as a Matrix,
either it won’t compile (the lucky case) or it will implicitly convert
to Matrix which will try to do the math (on objects which have
disappeared) and it’ll segfault (the semi-lucky case) or just give the
wrong answer (the worst case).
Falcou et al. want to solve this by letting the ExpressionTemplate class
indicate, “When I am captured by auto, here is the type I actually want
to be captured as.” In this case, we want ExpressionTemplate to be
captured as Matrix.
As mentioned in my previous post,
C++’s native reference types already behave as if they have an implicit
operator auto. When you auto a reference type, you don’t get the actual
reference type — you get the referenced type!
template<class T>
class NativeReferenceTo {
T *ptr;
// P0672 proposed this syntax, among others
T operator auto() const { return *ptr; }
};
Anyway, at the ACCU Bay Area C++ meetup
last night, in a conversation afterward I mentioned operator auto, and
Bryce Adelstein Lelbach informed me that operator auto was already in the language!
“That’s a deduction context,” he said. And he was right!
struct S {
operator auto() { return 42; }
};
int main() {
S s;
double x = s;
}
This is a valid C++14 program. But it doesn’t do what you’d expect (even if you expected it
to compile, which I certainly didn’t!). Here the word auto is essentially standing in for
the return type of the function, and then C++14’s “return type deduction” kicks in to deduce
the return type of the function as decltype(42) i.e. int. So this S is precisely equivalent
to
struct S {
operator int() { return 42; }
};
Notice that this is grossly different from
struct S2 {
template<class T>
operator T() { return 42; }
};
In case S2, the conversion operator’s return type is deduced from its call site, not from
its body. Converting S2 to X will always work, and always yield X{42}.
And then, if we combine both…
struct S3 {
template<class T>
operator auto() { return 42; }
};
…GCC helpfully warns us that this member function cannot ever be called, because it needs to
deduce T from its call site, and its call site needs to consider all possible operator Xs
to perform overload resolution, and its call site isn’t allowed to instantiate any of the
operator Xs until it’s picked the right one. In other words, this seems to be the one place
in C++ where the caller performs overload resolution based on a function’s return type instead
of based on its parameter types. So naturally this does not play well with C++14 return type
deduction.
If we give T a default, so that it doesn’t
need to be deduced, then GCC and Clang disagree: GCC says “I can’t figure out which operator X
you want me to instantiate first,” whereas Clang somehow eagerly instantiates enough of the function
body to find out its return type, and thereafter treats the auto as equivalent to int.
Modest proposal: let’s remove operator auto from C++, thus freeing up more real estate for P0672,
and also eliminating the source of blog posts like this one.
