Sign in to follow this  
Khatharr

Template-related compiler bug or... ?

Recommended Posts

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?

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites

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.

Edited by King Mir

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites

Yeah, I know it's the deduction failing. The example I posted is a reduction of a case I found in an external API, and I submitted something similar to that solution to them (they rejected it outright because they're retarded).

 

I'm just a little surprised that the deduction process doesn't account for promotion/conversion and just complain about ambiguity (or even just use overload rules) when it occurs. I suppose it's probably just seen as extra implementation work for a trivial edge case.

Share this post


Link to post
Share on other sites
Having done some implementation of templates for a compiler before (not in C++, thankfully) I can offer some speculative insight into why this is done this way.

Basically, consider the options. When trying to deduce a template substitution, you need to start with the set of all possible types, and whittle it down from there. If every ambiguous or problematic substitution into a template generated a warning (aka error), you'd drown in noise just trying to write a trivial template.

Why? Because there are always possible substitutions that don't make sense. Silently ignoring them is actually the best possible course of action, because it avoids spamming the programmer with needless noise that can't help them anyways.


You just happened to get caught in the crossfire here. You have an ambiguous or otherwise problematic deduction, and the compiler can - nay, should - silently ignore the substitutions that fail here. The end result is a cryptic barf.

There is a very, very thin argument to be made for remembering the problematic substitutions and printing them only in case of an actual barf. However, this is a bad idea because it will generate a ton of false-positives that you (the programmer) would need to sift through to find the actual cause of the problem.

Share this post


Link to post
Share on other sites

I'm just a little surprised that the deduction process doesn't account for promotion/conversion and just complain about ambiguity (or even just use overload rules) when it occurs. I suppose it's probably just seen as extra implementation work for a trivial edge case.


How else is it supposed to pick T given the context?

You seem to want it to guess T=float from substituting for Foo<T> and try that. But why is the left-hand parameter granted magic powers that the right-hand one doesn't have? We'd also want to guess T=int for the second parameter and try that overload, too, meaning we'd get an overload set. That is definitely not what you're going to want.

template <class T> void blah(T, T);

blah(1, 1.f); // would error: ambiguous between blah(int, int) and blah(float, float).

Share this post


Link to post
Share on other sites

I was thinking that Foo<T>& doesn't make sense to convert here. I definitely get your point about ambiguity in T, T case since that would clearly be the overwhelmingly common case and even non-template overload doesn't figure that out. It's not beyond reason to just declare that the deduction sequence is left-to-right (or any other arbitrary way), but that's just adding more weird edge behavior to the language, so it's probably better as it is.

Share this post


Link to post
Share on other sites

It's not beyond reason to just declare that the deduction sequence is left-to-right (or any other arbitrary way), but that's just adding more weird edge behavior to the language, so it's probably better as it is.


Left-to-right is not a workable fix, unfortunately. That would be a special rule handling operator*=(Ambiguous<T>, T) but which fails horribly on operator*(T, Ambiguous<T>). It'd be even more confusing than what we have today. :)

Language design is hard. If it wasn't, C++98 would've been perfect and we'd never need to release another version again. :P Edited by SeanMiddleditch

Share this post


Link to post
Share on other sites

fails horribly on operator*(T, Ambiguous<T>).

 

How do you mean? Like for (float, Ambiguous<int>)? What I was thinking is that in that case it would just look for an appropriate conversion/promotion from Ambigous<int> to Ambigous<float> and fail if there isn't one. Which, yes, is more confusing. That's what I was saying about weird edge behaviors.

 

I'm not certain if that's what you mean by Ambiguous<T> though.

Share this post


Link to post
Share on other sites

 

There's a few other ways to write that function, but that's probably the least crazy.

 

A third and not-so-crazy option is to make the type of the second parameter a non-deduced type.

#include <type_traits>
 
Foo<T>& operator*=(Foo<T>& left, typename std::identity<T>::type right) {
    ...
}

Passing the type through a dependent type like that excludes that T from the deduction process. The parameter instead participates in conversion once the actual type has been resolved.

Share this post


Link to post
Share on other sites

That may explain why I was surprised there wasn't much information about it. It's in VS 2013 at least where I tried it, but if it was actually removed then it should be fairly easy to make the necessary types to handle the particular problem raised in this thread.

template<typename T>
struct identity {
    using type = T;
};

The idea of making the second parameter a non-deduced one still applies.

Edited by Brother Bob

Share this post


Link to post
Share on other sites
Yes, implementing it as a local utility should be absolutely trivial, the 'std::' just threw me off since I neither knew about it nor could I find reasonable amount of information...

Share this post


Link to post
Share on other sites
template <class T>
class Foo
{
	public:
		T val;
};

template <class T>
Foo<T>& operator*=(Foo<T> & left, T right)	// 'left.val' and 'right' are considered 
{						// to be the same type (that is 'T').
	left.val *= right;			// So, if you give different types
	return left;				// for them, the compiler can't	 deduce 'T'
}						// since there are two candidates.

template <class T, class U>
Foo<T> & operator+=(Foo<T> & left, const U & right) // The solution.
{						    // Allow 'right' to be something else.
	left.val += static_cast<T>(right);	    // Now, 'T' may be equal to 'U', or not.
	return left;
}
Foo<float> bar;
constexpr int four = 4;
constexpr int five = 5;
bar *= four;	// Error: The compiler can't decide whether 'T' will be 'float' or 'int'.
bar += five;	// OK: 'T' is deduced to 'float' and 'U' is deduced to 'int'.
Edited by hkBattousai

Share this post


Link to post
Share on other sites
The reason I didn't recommend forcing the parameter to T was because that may, for some operations, result in an extra needless conversion (assuming Foo would ever be implemented over types other than int or float). The operation we care about here is the lhs.val *= rhs. That should be the only place that a conversion is required. Otherwise your requirements rise from "supports T*=U" to instead "supports T(U) and T*=T".

The solution hkBattousai posted is the same thing, just minus the explicit constraint. Which in retrospect is probably fine for 99% of people. I find it nicer to have constraints than to get horrible error stacks in template instantiation, but without C++ concepts, the constraint code is kinda complex and probably not worth it for most people.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this