2

I just started the topic of function templates today and I'm excited about the prospects.

Suppose I have something like:

template <class T>

T findGreater(T t1, T t2) { return (t1 > t2) ? t1 : t2; }

I can either pass two ints or two doubles to the function, but is there anything ultimate like I can pass an int and a double and it just returns the greater one. I know we get some ambiguity error for the function and all but is there a nice and smooth way of achieving this ultimate template functionality?

B-Mac
  • 557
  • 2
  • 14
  • You can use union to mimic it, but whatever you want to do, it is very evil. As you are programming a strong typed language, you should be aware of what type you are working on – SwiftMango Mar 15 '15 at 05:32
  • "Ultimate"? What do you mean by "ultimate"? All I can think of is "ultimate showdown of ultimate destiny." – Kyle Strand Mar 15 '15 at 05:40
  • This function is normally called `max` and it's a part of the standard library. The standard version only accepts arguments of the same type. Perhaps a `max` function that does more than that with argument types is not such a great idea. – n. m. could be an AI Mar 15 '15 at 05:48

2 Answers2

4

can pass an int and a double....

You can have multiple template types in a template function, yes:

template <typename LHT, typename RHT>
// ... function definition

...and it just returns the greater one

So what would the return type of this function be? The return type is part of every function declaration, and it must be determined at compile time. So the return type can't depend on the values of the arguments passed to it.

You could have the following:

template <typename LHT, typename RHT>
LHT findGreater(LHT lhs, RHT rhs) { // ...etc...

...which assumes that LHT is an acceptable return type; you will lose precision if LHT is integral but RHT is floating.

In C++11 and beyond, you could use decltype, which should prevent you from losing precision:

template <typename LHT, typename RHT>
auto findGreater(LHT lhs, RHT rhs) -> decltype(lhs + rhs) { // ...etc...

Here, I've used the + operator because it resolves to the higher-precision type regardless of the order of the arguments.

But what if instead of +, you just used decltype((lhs > rhs) ? lhs : rhs)? Here, by simply copying the body of the function into the decltype expression, you're essentially telling the compiler "the return type that is returned by this function," which is...what, exactly?

Well, the type of a ternary expression is a common type of its arguments; in the case of the built-in numeric types, this means that the lower-precision type will be promoted. So this appears to be equivalent to using +.

However, in C++ (unlike in C), ternary operators can evaluate to lvalues; decltype always deduces reference type for lvalues, so if LHT and RHT are the same type, the function will return a reference to either lhs or rhs.

There are two ways around this:

  • In C++14 and beyond, you can leave off the decltype, as long as the function is defined where it is declared. auto does not deduce reference-type for the lvalue ternary operator evaluation.
  • Use std::decay on the decltype to ensure that it does not resolve to a reference:

    template <typename LHT, typename RHT>
    auto findGreater(LHT lhs, RHT rhs)
    -> typename std::decay<decltype((lhs > rhs) ? lhs : rhs)>::type
    {
        return (lhs > rhs) ? lhs : rhs;
    }
    
  • Pass your arguments by reference and intentionally return a reference. This would look like this:

    template <typename LHT, typename RHT>
    auto& findGreater(LHT& lhs, RHT& rhs)
    {
        return (lhs > rhs) ? lhs : rhs;
    }
    

    Note that I have explicitly chosen auto& as the return type here. This ensures that the return type is a reference type even if LHT and RHT are different types (i.e. if decltype((lhs > rhs) ? lhs : rhs) does not evaluate to reference type). This is apparently not viable in C++11; see comments below.

Community
  • 1
  • 1
Kyle Strand
  • 15,941
  • 8
  • 72
  • 167
  • Thank you for your detailed explanation. Just want you to know that I really appreciate your effort. Thank you. :) – B-Mac Mar 15 '15 at 06:10
  • "In C++14 and beyond, this is equivalent to leaving off the `decltype`" - no. `auto f(...) -> decltype(stuff) { return stuff; }` is equivalent to `decltype(auto) f(...) { return stuff; }` barring SFINAE considerations; `auto f(...) { return stuff; }` is something different. – T.C. Mar 15 '15 at 06:15
  • I see that you have used `typename` instead of `class`. What's up with that, bro? :) – B-Mac Mar 15 '15 at 06:16
  • Also, which version of C++ is the latest one? – B-Mac Mar 15 '15 at 06:16
  • @T.C. It's equivalent in this case, though. I suppose it is worth editing to clarify. – Kyle Strand Mar 15 '15 at 06:36
  • @Grendan I prefer `typename` to `class` in that context because (1) `class` is used to define new classes, while `typename` makes it obvious that I'm referring to an *existing* type; and (2) I see `typename` used much more frequently than `class` in that context. There are [other](http://stackoverflow.com/q/2023977/1858225) [questions](http://stackoverflow.com/q/213121/1858225) about this. – Kyle Strand Mar 15 '15 at 06:42
  • @Grendan C++11 and C++14 refer to two versions of the C++ *standard*, which is a long document created by a committee to describe precisely how the language works (i.e. precisely how conforming compilers must behave). C++14 is the latest standard, but not all compilers support all of the new features yet (GCC should support them in the next release; I think Clang supports all C++14 features already). – Kyle Strand Mar 15 '15 at 06:56
  • The next standard, which is currently being drafted, is known as "C++1z," and is expected to be released in 2017, in which case it will be known as "C++17." (Similarly, C++11 was called C++0x during its development, and C++14 was called C++1y.) – Kyle Strand Mar 15 '15 at 06:58
  • You actually want to return `auto` here, not `decltype(auto)`. Returning `decltype((lhs > rhs) ? lhs : rhs)` will break horribly if `lhs` and `rhs` have the same type. – T.C. Mar 15 '15 at 07:03
  • @T.C. Er...break horribly? What do you mean? – Kyle Strand Mar 15 '15 at 07:07
  • If they have the same type, that conditional expression is an lvalue, and `decltype` deduces a reference type. http://coliru.stacked-crooked.com/a/4238d5e0b7ac549d – T.C. Mar 15 '15 at 07:17
  • When you use a trailing return type, you can only write `auto`, not `auto&` or other things. The fix in C++11 is to `std::decay` the `decltype(...)`. – T.C. Mar 16 '15 at 17:49
  • @T.C. Hm, you're right. `std::decay` requires a type-specifier, though, and I'm not sure how that can be specified here. – Kyle Strand Mar 16 '15 at 20:37
  • `typename std::decay rhs) ? lhs : rhs)>::type`. – T.C. Mar 16 '15 at 20:47
  • I still don't like the third option. It makes the function not usable with rvalues (`findGreater(x, 1)` won't work), or with most mixed types, since for those the conditional expression yields an rvalue that can't bind to a (non-const) lvalue reference. It is a viable option if the arguments have the same type, and written as a member of an overload set. (i.e., it would handle cases where `?:` returns a lvalue, with a separate overload handling mixed-type or rvalue argument cases) – T.C. Mar 16 '15 at 22:53
1

No, a return type can't change based on run-time values. The return type, and all template types, are determined in compile time.

Think about this scenario:

void func(MyClass n);
bool operator>(const MyClass& l, double r);
/*...*/
func(findGreater(myClassObject, myDouble));

Now, this should be a compilation error if myClassObject is not > myDouble, but how can you have a compilation error in run time? It could throw some kind of exception, but this is going down a hole where the language provides some major run time features where it's basically valid to incorrectly call any function and the program will still compile, but cause a runtime error if actually called. This is not C++-like at all. C++ is strongly typed. You have to pass valid types in all scenarios to successfully compile.

Also fyi you basically just wrote std::max

David
  • 27,652
  • 18
  • 89
  • 138