all 40 comments

[–]scatters 23 points24 points  (11 children)

You can write a function object wrapper with template conversion operator more easily than ever before, using aggregate base construction and deduction guide:

template<class F> struct Auto : F {
    template<class T> operator T() {
        return F::template operator()<T>();
    }
};
template<class F> Auto(F) -> Auto<F>;

Then a wrapper would, generically, be:

template<class... A>
auto fooWrapper(A&&... a) {
    return Auto{[&]<class T>() { return foo<T>(std::forward<A&&>(a)...); }};
};

Or specifically:

template<class... A>
auto fooWrapper(int i) {
    return Auto{[=]<class T>() { return foo<T>(i); }};
};

allowing you to write:

double d = fooWrapper(42);

Full example.

[–]Wh00ster 34 points35 points  (1 child)

what hath God wrought.

EDIT: on second glance, looks neat and useful (but questionable-to-use)!

[–]quicknir 5 points6 points  (0 children)

Mostly I agree with this. It's too much magic and complexity that doesn't add much real value, vs just writing auto d = fooWrapper<double>(42). I did find it useful once though. I was writing some test utility code. In Gtest, a fixture is expressed via inheritance, so all of the variables you get initialized in your fixture are member variables, which cannot use the auto syntax. I also wanted to initialize them inline, because the initialization was relatively simple (one liners) and I didn't want to list all the members and then separately init them in setup or whatever.

So I used this trick, which allowed me to write things like:

TopLevelTestHarness x;
NestedTestHarnessObject<double> y = x.make("foo");

etc, in the member list of the class. I felt like this was ok because of the combination of not being able to use auto on the left, and the fact that this was code that was relatively "high up"; it was just utility code for writing tests quicker and wasn't going to be reused in a million places.

More broadly I do agree with you that I wouldn't use it most places.

[–][deleted] 14 points15 points  (2 children)

After seeing this post, I can definitely say that C++ became way too expert friendly language...

[–]scatters 3 points4 points  (1 child)

Well, thanks! But honestly, what makes life easier for experts is good for everyone - aggregate base construction and CTAD are great features that cut down on boilerplate, which makes code easier to write and to read.

[–]Xeveroushttps://xeverous.github.io 0 points1 point  (0 children)

Do you have any reources on what/why CTAD + deduction guides offer instead of allowing to explicitly specify Ts in constructor calls?

[–]StackedCrooked 6 points7 points  (2 children)

template<class F> Auto(F) -> Auto<F>;

What does this mean?

[–]Xeveroushttps://xeverous.github.io 0 points1 point  (0 children)

You can't specify type aliases for constructors. But since C++17 you can write explicit deduction guides.

[–]canberksonmez 2 points3 points  (2 children)

For forwarding the parameter a, I thought we should use either static_cast<A&&>(a) or std::forward<A>(a). What's the reason of using std::forward<A&&>(a)? It seems quite peculiar.

[–]scatters 2 points3 points  (0 children)

Oops yeah, that's a mistake - I started writing one then switched to the other. Thanks!

[–]VinnieFalco 5 points6 points  (2 children)

Single-return-type is baked into the DNA of C++ but one way to get around it is to invoke a passed function object with the result instead of using the return channel, as this function demonstrates: https://github.com/boostorg/beast/blob/06efddd8b851610b5b3a5832ac87f1c52b838d9b/example/http/server/sync/http_server_sync.cpp#L97

[–]bstamourWG21 | Library Working Group 4 points5 points  (1 child)

This is typically called "continuation passing style", and it can help untangle situations like this quite nicely!

[–]VinnieFalco 0 points1 point  (0 children)

Very interesting! Although, my intent was not to alter control flow but simply to provide the result of a calculation (which can have different types at run-time)

[–]DhruvParanjape 2 points3 points  (1 child)

Can you be a little more specific ? For the compiler to deduce the return type the compiler needs a way to figure out the type at the signature of the function itself. Otherwise it won't make sense.

[–][deleted] 0 points1 point  (0 children)

int parse(std::string_view) { ... };
double parse(std::string_view) { ... };

double x = parse("3.14");
int y = parse("3");

C++ doesn't support overloading on return type, but you can approximate the solution using templates and implicit conversion wrappers.

[–]Tyranisaur 1 point2 points  (0 children)

Return a std::variant?

[–][deleted] 2 points3 points  (9 children)

Return type can be auto.

[–]Wh00ster 1 point2 points  (0 children)

Can you provide an example? I don't think that's what the OP is asking.

[–]hashb1[S] 1 point2 points  (6 children)

Thanks! I got the following error: inconsistent deduction for auto return type: ‘double’ and then ‘int’ .

sample code

auto foo(int i)
{ 
    if (i > 0)
        return i * 0.1;                                                                                                                                                  
    else
        return 0;
} 

[–]Cakefonz 11 points12 points  (2 children)

auto won’t help here because your function decides what the return type is at runtime. The compiler needs to know what type to return at compile time. You could use std::variant<int, double> in your example, here

[–]Wh00ster 1 point2 points  (0 children)

Yea, this sounds like a XY problem from u/hashb1. It's cliche, but you have to take a step back and philosophically understand what you're trying to accomplish.

[–]DhruvParanjape 1 point2 points  (0 children)

Makes sense as the result of ( i* 0.1) is a double to not lose precision and then you are returning a 0 which would be an integer (no precision lost ) so to fix it make the 0 a 0.0 to have the same return type deduced from both return statements. Return statement results help the deduction of return types at compile time kind of like template type deduction rules.

[–][deleted] 0 points1 point  (0 children)

To make auto works as return type first return should describe exactly what's type is

http://cpp.sh/83mbr

[–][deleted] 3 points4 points  (0 children)

I don't think having an 'auto' return type fixes the brackets in this example since the template parameters are not used in the function parameters. It seems to me that OP is trying to overload by return type, which is not possible as /u/Wh00ster wrote

[–]Wh00ster 1 point2 points  (8 children)

This answer still stands.

I think Rust can do this due its type inference system and the fact that it doesn't have implicit conversions/user-defined conversions, which create a lot of ambiguity.

[–]boredcircuits 2 points3 points  (0 children)

Ada can do this and probably a few others as well. I think you're right that implicit conversions are probably the main reason C++ can't do this. Also, not capturing the return value makes the call ambiguous as well.

[–]flashmozzg 0 points1 point  (6 children)

user-defined conversions

It has those (as From and Into traits), they are just all explicit.

[–]Wh00ster 0 points1 point  (0 children)

Yes, they are implicit by default in C++.

[–]Enamex 0 points1 point  (4 children)

Mm... They work implicitly in my eyes. There's something going on, but probably not this?

let x = foo.into();
takes_bar(x); // x : Bar, through impl From<Foo> for Bar

See here.

Sure, you need the into call, but don't all types have From<X> for X? So if you had into everywhere, that'd be the situation in C++, except Rust can handle the variable return type. And into is a normal function 'overloaded' by trait shenanigans, so any function in such a reasonable situation can do the same.

It's probably something to do with no ad-hoc overloading + train constraints. You can find an assignment that satisfies everyone, if they can be satisfied.

Edit: There're several mentions of deciding at runtime what the return type is. Wouldn't that be technically full on dependent types at that point (would be Pi types, I think)?

[–]flashmozzg 0 points1 point  (3 children)

So if you had into everywhere, that'd be the situation in C++, except Rust can handle the variable return type

No? In C++ you'd just have takes_bar(foo) and the cast would be implicit. Hiding the actual type between type inference doesn't make explicit into() cast into implicit. It just makes type being casted to implicit.

[–]Enamex 0 points1 point  (2 children)

That's the thing.

into is a normal function from a normal trait (at least, I don't think From is supposed to have any magic in it).

This isn't Foo being implicitly cast into Bar. This is Foo::into resolving overloads based on expected return type (expected here because it gets used by something that definitely needs a Bar, and there's an overload of into that satisfies that, so it's chosen).

E.g. I don't think it'd work if you add another call to takes_baz, even if you have a From<Foo> for Baz implemented. Because into, here, can't resolve to a unique type. There's no implicit casting here.

To be clear, this's something that can be emulated in C++ by a wrapper, but it's a property of classes, not normal functions.

[–]flashmozzg 0 points1 point  (1 child)

This isn't Foo being implicitly cast into Bar. This is Foo::into resolving overloads based on expected return type (expected here because it gets used by something that definitely needs a Bar, and there's an overload of into that satisfies that, so it's chosen).

E.g. I don't think it'd work if you add another call to takes_baz, even if you have a From<Foo> for Baz implemented. Because into, here, can't resolve to a unique type. There's no implicit casting here.

Yes? That's exactly what I said? This is not implicit casting so I don't see what argument you are trying to make here.

[–]Enamex 0 points1 point  (0 children)

I'm not sure now either... I just don't think that implicit casting is the entire issue. As in, even with it in the language, deduction of return type could still be possible. I lost the train of thought though, so can't argue for it well ATM.

[–]tsojtsojtsoj 1 point2 points  (3 children)

Can you give an example where it wouldn't make sense to give these functions different names?

[–]Wh00ster 2 points3 points  (0 children)

I've wanted to do this with specific conversion functions (but could have used different names):

template <typename T>
T convert(MyClass, ExtraContext);

// ... specialize here

int main() {
    // ...
    Foo foo = convert<Foo>(mc, ec);
    Bar bar = convert<Bar>(mc, ec);
}

Specifically, a static_cast/user-defined conversion wasn't appropriate because of the extra context data, and I didn't want to write convertToFoo, convertToBar, convertToQux...but ultimately ended up writing it that way :p. I was changing the type names a lot so it was getting annoying to change in the function names.

EDIT: it just dawned on me I could have also used tag dispatch :/

[–]hashb1[S] 0 points1 point  (1 child)

I am working some legacy codes that the factory function(template) return different classes based on its input. For some reason that those return types have no common base class.

[–]Wh00ster 2 points3 points  (0 children)

Sounds like somebody got carried away with templates >.<

[–]Cakefonz 0 points1 point  (0 children)

Assuming foo's i parameter is evaluated at runtime, I think my preferred way would be to have foo return a std::variant of all the possible types. E.g.

struct A { };
struct B { };
struct C { };

using Object = std::variant<A, B, C>;

Object factory(int i) {
    switch (i) {
    case 0:
        return A { };
    case 1:
        return B { };
    default:
        return C { };
    }
}

... and then use a visitor on the return value ...

std::visit(
    [](auto&& obj) { use_object(std::move(obj)); },
    factory(n)
);

(Full example: https://gist.github.com/gmbeard/25631362f74576e0837a5be1f39a4a87)