views:

253

answers:

4

The following code does not compile:

#include <iostream>
class Foo {
  std::string s;
 public:
  const std::string& GetString() const { return s; }
  std::string* GetString() { return &s; }
};

int main(int argc, char** argv){
  Foo foo;
  const std::string& s = foo.GetString(); // error
  return 0;
}

I get the following error:

const1.cc:11: error: 
invalid initialization of reference of type 'const std::string&' 
from expression of type 'std::string*

It does make some sense because foo is not of type const Foo, but just Foo, so the compiler wants to use the non-const function. But still, why can't it recognize that I want to call the const GetString function, by looking at the (type of) variable I assign it to? I found this kind of surprising.

+10  A: 

The return type is determined from the overloaded function which is actually called, it never forms a part of overload resolution itself. (What if the return type wasn't used?)

const isn't the problem with the return value as you can bind a non-const object to a const reference, it's the fact that your function is returning a pointer which you don't dereference.

As foo is not const, the non-const GetString() is called - it is a better match for a non-const object. You need:

const std::string& s = *foo.GetString();
Charles Bailey
+3  A: 

I can't recall exactly why they don't allow overloading on return type (I think it's because return values can be discarded and thus the function wouldn't be distinct), but you can fix the problem with a const_cast hint to the compiler: const std::string& s = const_cast<const Foo&>(foo).GetString();

Mark B
Chose this answer as accepted answer because it actually explains why the return value cannot be taken into account.
Frank
I *think* it's because it was considered a nice property that the type of a sub-expression doesn't depend on context (well, except as it affects name-resolution), just on the sub-expression itself. Actually there are a very few exceptions in C++, relating to selecting the referand when you take a pointer to an overloaded function.
Steve Jessop
@Frank: It doesn't explain "why the return value cannot be taken into account". And obvious counterargument would be that only if the return value is used, then it should be taken into account. The only real answr is that it is simpler that way. Incuding the return type into overload resolution would makes things more complicated.
AndreyT
One reason you can't overload on return type because the mangled form of the name (which is used by the linker to differentiate the various overloaded functions) does not encode the return type, just the parameter types. I'm not sure how much stuff will break if you change name mangling to add in the return type; probably all C++ shared libraries (e.g. boost) would need a major version number bump since this would break all backward compatibility. (ALL names are mangled, not just overloads. The only exception is `extern "C"` declared functions, which cannot be overloaded.)
Mike DeSimone
@Mike DeSimone: Isn't that a bit of a circular argument? Surely, if the language allowed overload on return type then the return type would have to be included in the name mangling. The language requirements driving the name mangling systems, not the other way around.
Charles Bailey
I wasn't saying it had to be that way, just that it was and is that way. It's a legacy item, and there's nothing stopping the C++0x folks from changing it if they can nail down how return type overloading is supposed to work.
Mike DeSimone
I think that's the problem though. It could potentially introduce much more ambiguity in function call chains overloaded both on parameters and return type.
Mark B
+1  A: 
Ben Voigt
I don't get the example. The overload for evaluate would be ambiguous, and you would have to help out the compiler with a cast (if overloading on return type were allowed).
UncleBens
The question expected the compiler to infer the return type based on context. The example isn't ambiguous (if you still think it is then list all the possible matches) but very difficult for the compiler to figure out. The general case of return type inference in C++ is equivalent to the halting problem.
Ben Voigt
What about: `size_t result = sizeof(thingamatize(std::abs(4.0f)).value('B'));` It doesn't figure out that it should convert the result of `abs` to `double` either, so the general problem does not seem to be about overloading based on return type. - Your example seems just to show that mind-reading abilities can't be expected from a compiler, not a technical reason why return types *couldn't* be used in overload selection.
UncleBens
There won't be a conversion of the result because the compiler can always synthesize the template function `thingamatize` to accept the exact type. With `std::abs`, the return type is the same as the argument type. There's exactly one of the `evaluate` overloads which makes the expression meaningful, but it'd be a bear for the compiler to figure out which.
Ben Voigt
Why would the compiler have to figure out a way to make it meaningful? As I see there are two possible results. `4 - evaluate(myFoo)` itself is ambiguous, because you can do both `int - int` and `int - double`. If it strongly favoured the first, then the call to `abs(int)` would be ambiguous. - To make it select the overload you intended, you'd have to do: `4 - static_cast<double>(evaluate(myFoo));`. It is not uncommon that you have to guide the compiler which overload to select with the use of casts.
UncleBens
To make the point clearer: there already is a way to "overload based on return type". Add a class with conversions: `struct Conversible {const Foo Conversible(const Foo } operator double() const { return f.y; } operator std::complex<char>() const { return f.z; }};` Change `evaluate` to return an instance of this. Now you will find that `4 - evaluate(myFoo)` is indeed ambiguous (it could apply conversion both to `int` and `double`), and correct overload can be forced with a cast.
UncleBens
Ben Voigt
What? All it takes is to determine the type suitable to the immediate context where it is used `4 - evaluate(myFoo())` (which would be ambiguous with given overloads). I have no idea why you think the compiler should solve halting problems. The behavior needn't be any different from selecting between multiple overloaded conversion operators. - Simply put, if the feature existed, your example would just be malformed code.
UncleBens
You've missed the fundamental difference between user-defined conversion and the general case of a function. User-defined conversions all take a single hidden "this" parameter of identical type. OTOH overloaded functions can take multiple arguments with different types. This means that type inference is forward-only for conversions but operates in both directions when return type inference is allowed.If you levy the same restrictions as for conversions (i.e. return type overloading is only done between functions with the exact same arguments) then it doesn't apply to the original question.
Ben Voigt
Whatever. I'll probably just never understand your reasoning, because I don't get the premise **why** your example should be valid code.
UncleBens
It isn't valid, and shouldn't be, because overloading on context is impossibly difficult in the presence of templates. This was just a short illustration of how hard a problem comprehensive return-value overloading actually is.
Ben Voigt
And I'm saying it's not particularly hard if you don't get your expectations unreasonably high.
UncleBens
So what are you proposing is a reasonable expectation? That return-value overloads be done only in the final step of the RHS of an assignment operator? What if the assignment operator is overloaded to accept more than one type? The issue is that currently overload resolution operates on a directed acyclic graph. When you add return type overloading, the overload resolution graph becomes undirected, as type information flows in both directions. And many questions that are easy to determine on DAGs are NP-hard on undirected graphs.
Ben Voigt
A: 

The return type of a function is not used when selecting the overload. That's just how the language works.

However, implicit conversions are selected based on the context. So, technically you can make it compile by returning something that is implicitly convertible both to a reference and a pointer.

#include <iostream>
#include <string>

struct Evil
{
    std::string* p;
    Evil(std::string* p): p(p) {}
    operator std::string*() const { return p; }
    operator std::string&() const { return *p; }
};

struct ConstEvil
{
    const std::string* p;
    ConstEvil(const std::string* p): p(p) {}
    operator const std::string*() const { return p; }
    operator const std::string&() const { return *p; }
};

class Foo {
    std::string s;
public:
    ConstEvil GetString() const { return ConstEvil(&s); }
    Evil GetString() { return Evil(&s); }
};

int main(int argc, char** argv){
    Foo foo;
    const std::string& s = foo.GetString(); // ok
    return 0;
}

But the real answer is that functions overloaded on constness should have a similar return type. (I suppose this is where the convention "use pointers for mutable things, const references for immutable things" just breaks down.)

UncleBens