Template-related compiler bug or... ?

Started by
22 comments, last by King Mir 8 years ago

In VS 2015 I'm getting an error that I didn't expect and I'm wondering if this is a bug or if it's not expected to work.

The issue itself is trivial. I'd have used a cast in any case to suppress the warning, but I'm wanting to know whether I should report the bug to MS.

Code to recreate:


template <typename T>
struct Foo { T val; };

template <typename T>
Foo<T>& operator*=(Foo<T>& left, T right) {
  left.val *= right;
  return left;
}

void main() {
  Foo<float> bar;
  bar *= 4; //"no operator *= matches these operands"
}

I get why it's happening, but I kind of expect that when it doesn't find a float,int signature it will make the effort to check promotions.

Is this standard?

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.
Advertisement
Should the left.val *= right actually have right.val?

Because otherwise, whatever type T is needs to be implicitly convertable to something that can be multiplied by left.val. And that structure is not.

My bad, I was fiddling with it. I'll fix it in the OP. 'left' should be Foo<T>&.

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

C++ template parameter deduction rules are smart, but there are limits. Essentially you ran into one. Each template function parameter is independently used to deduce the type of T. If different parameters disagree on the type of T, then the template is not considered.

C++ template type deduction rules are spelled out in the standard in excruciating detail, and compilers implement it exactly as specified; they cannot be any more or less smart about it because it would change the semantics of the language. While bugs are possible, you would need to be well versed in the C++ standard to differentiate between compiler bugs and C++ limitations.

C++ template type deduction rules are spelled out in the standard in excruciating detail

Okay, so are you going to cite a relevant passage so the question is answered?

and compilers implement it exactly as specified

Except for when they don't.

While bugs are possible, you would need to be well versed in the C++ standard to differentiate between compiler bugs and C++ limitations.

There are people here that are extremely well-versed, so I'm asking those people.

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.
I told you the rule that is applied, but I cannot cite the standard. If you require that level of rigor, I recommend reading the standard yourself.

You also told me that compilers always implement the standard correctly and asserted the reasoning that it's not possible to find a bug unless you've memorized the standard.

I am searching the standard right now, but I'm also allowed to ask other people that are more familiar with it in case someone knows with a satisfactory degree of confidence.

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

The behavior is standard. The relevant section is §14.8.3, paragraph 1.

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.
The rules limit it to one implicit conversion and one user-supplied conversion. This is already enough to nearly overwhelm some compilers with large code bases. Being able to consider a larger number of conversion chains would have an enormous search space when trying to figure out conversions for template types. So this can work if you reduce the levels of indirection by one.

The rules also only allow the implicit conversions when used explicitly as a parameter, (so if you specified the value was a float explicitly) but it does not need to consider all implicit conversions when doing template argument deduction, which can get quite complex and compilation complexity would be worse if it needed to consider everything that could possibly be implicitly converted.

You say you already see what you need to do. Make the number a float rather than relying on an implicit int to float conversion, as you know you should. With no implicit conversion, template argument deduction will work exactly as it should.

Template-related compiler bug or... ?


The answer to that question is basically always the OR option.

Compiler bugs do happen. Unless you're really pushing the compiler with cutting-edge Boost-developer style magic, though, you're not hitting a bug. You're just doing it wrong. :)


The solution for your case would be to change up the template definition to not rely on type deduction. You're making assumptions based on the rules for overloading, but overloading isn't want has failed. Type deduction is what has failed. The operator*= is being excluded because of substitution failure.

Basically, the problem is that the compiler has to guess what T is for your operator*=. The parameters it has to match against are Foo<T> and T, and you've passed it a Foo<float> and an int. T cannot be both a float and an int. Substitution of types for T has failed and the template is not added to the overload set. There is then an empty overload set. Promotion only comes into play when _converting_ values, not when substituting types, but since there's an empty overload set there's nothing to promote anyway.


One option would be to put the operator*= inline in the defintion of Foo. That way, T no longer needs to be deduced because it is explicit in the definition of Foo<float>, the operator*= of type T is found and added to the overload set, and the int is then promoted to float during overload resolution.


A second option, if the definition must be external, is to allow more flexibility for substitution. There are several ways to do this, but I think the "simplest" one may be this:

template <typename T, typename U, typename = decltype(std::declval<T>() *= std::declval<U>())>
Foo<T>& operator*=(Foo<T>& a, U const& b)
{
  l.val *= b;
  return l;
}

The third typename parameter will trigger substitution failure when a value of T (such as l.val) does not support operator*= with a value of U (such as the b parameter). Concepts would make this code a lot cleaner, of course. There's a few other ways to write that function, but that's probably the least crazy. The cleanest option in general for this sort of problem relies on treiling return types or C++17 void_t, but trailing return types doesn't work in this specific circumstance and you odn't have void_t yet. :)

Sean Middleditch – Game Systems Engineer – Join my team!

This topic is closed to new replies.

Advertisement