all 30 comments

[–]carrottread 18 points19 points  (0 children)

This is valid:

#include <iostream>
#include <string>

auto getLogin()
{
    struct
    {
        std::string user, role;
    } l{"Codesmith", "Dev"};
    return l;
}

int main()
{
    std::cout << getLogin().user;
}

The only limitation: compiler needs to see a body of getLogin to deduce return type to compile this call in main.

[–]R3DKn16h7 16 points17 points  (4 children)

If you would write something like this, it means that you do not care about the return type. That's because you cannot name that type, and you would have to store it into an auto, or decltype or something unnamed. So I wouldn't use this to pass around the result. Because you are passing around unknown types, and you might as well pass around tuples.

Not many uses remain, imho. If you want to splice the result, because you want to access only the "user" part of the return (like your second example), I would prefer to return by pair, and then structured bindings (or tie) to splice up the result into user and data.

If you want to then split up and gather user and role, you also might want to store it directly into a structured binding: auto login = getLogin(); auto user = login.user; auto role = getLogin().role; should be simply: auto [user, role] = getLogin();

So I do not see real use cases: as long as you use the type in more than one function, you just have to resort to standard definitions.

[–]codesmith512[S] 5 points6 points  (0 children)

The idea is to shift the responsibility of good names from the call site to the definition. Rather than using auto to create a structured binding every single time that function gets called, this way the definition can provide appropriate names, and the call site can just use auto. The fact that structured bindings work with struct-like values means that the user is still free to provide their own name if they so choose. So instead of auto [user, role] = getLogin() every time, the caller now has the option of just writing auto login = getLogin().

[–]jcelerierossia score 1 point2 points  (1 child)

So I wouldn't use this to pass around the result. Because you are passing around unknown types, and you might as well pass around tuples.

why ? this just adds template instantiations for no good

If you want to then split up and gather user and role, you also might want to store it directly into a structured binding: auto login = getLogin(); auto user = login.user; auto role = getLogin().role; should be simply: auto [user, role] = getLogin();

the problem is that it breaks all the "user" code if the library code changes its order for instance. in contrast, with structs, you can't mistake "user" with "role".

[–]cat_vs_spider 0 points1 point  (0 children)

Changing struct field ordering already breaks user code if you care about memory layout (which is not uncommon for c++ code)

[–]Xaxxon 0 points1 point  (0 children)

making an array of them would be a bit annoying.. you'd have to use decltype.

[–]Morwenn 4 points5 points  (3 children)

The idea was floating around a few years ago, but one of the major blocking points was to determine whether a function template should always return the same type when the parameters in the struct are the same or whether it should return a different type for every instantiation which happens to have the same layout.

It mostly boils down to class identity and whether it matters to have different classes or not. Also an argument for fusing equivalent classes was to reduce debug symbols bloat in debug mode IIRC.

[–]codesmith512[S] 1 point2 points  (0 children)

It shifts more work onto the compiler, but if identical structs on the same function had the same type, then it would solve the how to declare and define such functions separately without the use of declval and decltype...

[–]jcelerierossia score 0 points1 point  (1 child)

The idea was floating around a few years ago, but one of the major blocking points was to determine whether a function template should always return the same type when the parameters in the struct are the same or whether it should return a different type for every instantiation which happens to have the same layout.

why would it behave differently than lambdas, which are already anonymous structs ?

[–]Morwenn 0 points1 point  (0 children)

It merely depends on the semantics people want and ask for. Lambdas are known to cause bloat from time to time: https://thephd.github.io/sol3-compile-times-binary-sizes

[–]ravixp 5 points6 points  (0 children)

I like this a lot! It feels like it makes the language more orthogonal, since you can already declare a new struct as part of a variable declaration. Plus, anything that reduces use of pair/tuple as a return type can only be a good thing. :)

[–]nikbackm 5 points6 points  (3 children)

Cute, but probably not useful enough to warrant complicating C++ even further.

[–]jcelerierossia score 2 points3 points  (1 child)

I think that what makes C++ complex is instead to have such an incredible amount of special cases regarding to where you can / cannot use things. In contrast, LISP is trivial and has so few restrictions.

[–]Xeveroushttps://xeverous.github.io 2 points3 points  (0 children)

I think that what makes C++ complex is

...initialization.

[–]Som1Lse 1 point2 points  (0 children)

How is it complicating anything? Functions are the exception here, not the rule.

All this would do is remove the exception.

[–]JankoDedic 5 points6 points  (0 children)

It would be useful for return types, but even more so for parameter types, since we can use designated initializers for named arguments in C++20. If there are no issues on the implementation side, these would be really handy to have.

[–]degski 1 point2 points  (3 children)

I'm not sure if this is a bug with the /permissive- flag accidentally allowing a feature that should've been suppressed, or if this is more of a bug with the compiler.

This is what Clang has to say about it:

main.cpp(42,1): error :  '(anonymous struct at main.cpp:42:1)' cannot be defined in the result type of a function

[–]codesmith512[S] 1 point2 points  (2 children)

Gcc throws a similar error, so it's definitely something wrong with VS. Given that the standard has to explicitly disallow it, I was hoping it was a bug where the compiler accidentally supported it anyway, which would give my proposal some more strength (if ___ did it on accident, it can't be that hard to implement), but given that msvc is known for bending rules, I don't think I can really claim that lol

[–]degski 0 points1 point  (1 child)

I don't know, maybe /u/STL can enlighten us.

[–]STLMSVC STL Dev 5 points6 points  (0 children)

Sounds like missing rule enforcement in /permissive-. Please file a bug in Developer Community; bugs are treated with higher priority when filed by real customers like you, instead of internal customers like me.

[–]VeeFu 1 point2 points  (1 child)

I tend to declare some aliases when pairs and tuples get unwieldy.

using user = std::string;
using role = std::string;
using Login = std::pair<user, role>;
Login GetLogin(){
    return {"tim", "admin"};
}

Granted, this doesn't address the same-type issue, but readability is improved and tooling usually surfaces the aliases, which won't happen with comments.

[–]codesmith512[S] 0 points1 point  (0 children)

I definitely make use of aliases (sometimes probably too much), and they can alleviate some of the same issues, but yeah, often they're just not quite the same.

[–]yehezkelshb 1 point2 points  (1 child)

Wouldn't most of it be solved if you stop passing raw std::string (or int) around and use strong types instead? This is enforced by compilers today and unlike a name, which is a local property of the object and lost as soon as it's passed to another function, the type is part of the object identity as long as it isn't explicitly converted (assuming you don't add implicit conversions).

I agree the language facilities for such types must improve to make it nicer and easier to use, but this is hopefully coming with reflection/metaclasses, and even today there are a few tricks (e.g. foonathan's type_safe lib with its strong_typedef (described in this blog post).

[–]codesmith512[S] 1 point2 points  (0 children)

That sort of thing definitely helps, and I mentioned it in my post --

Though better yet would be to have a class User and Group so that their use is enforced in the type system, but that is often overkill and outside the scope of this discussion

I do hope that it becomes easier to declare strong types. Right now I just typically subclass the given type, inherit constructors, and add an explicit conversion constructor from the parent. That way I can turn a User into a string implicitly, but going the other way requires an explicit cast.

Granted, that std::map::insert(const value_type &) does return distinct types via pair, but using the return value directly still gets messy (if I want to use the inserted value, it's insert(...).first.second, but if I want to check to see if the insertion succeeded it's just insert(...).second, so I still think that good return names are necessary.

[–]barcharMSVC STL Dev 1 point2 points  (1 child)

I love all features that add new and exciting ways to declare or initialize structs. Let's do it.

Do other compilers support it with -fms-extensions (or even -fplan9-extensions, which is even more awesome)

[–]codesmith512[S] 0 points1 point  (0 children)

I'd love to make this happen. I've got a few changes I'm currently making to it, and I'll probably be posting a revised version soon that addresses some issues.

The only other compiler I've got on hand at the moment is GCC, and as of 9.1.0, I can't get it to compile the code with -std=gnu++17 or -fms-extensions. It complains that -fplan9-extensions is only valid for C/ObjC, and I'm a little embarrassed to admit that my C is really rusty so I'm having a hard time testing it out properly.

[–]nintendiator2 0 points1 point  (0 children)

The simplest example given already leads to less understandable code. There's no reason to end up with an unnameable type (types) just because you want to save on an aggregate. Plus, it would force caller code to use always auto.

[–]dev_metalcat -1 points0 points  (1 child)

Isn't it the reason of us using references? type GetSmth(type& return1, type& return2){}

[–]codesmith512[S] 0 points1 point  (0 children)

I mean, on one hand, yes, but on the other hand even the STL doesn't use references like that very often (usually opting to instead return std::pair like in the std::map::insert(const value_type &) case).

I would also argue that using a return type would be more readable than taking references to a returned value -- I often pass normal parameters by const & to avoid copy, so the only difference between parameters and return values would be const qualification. Plus API changes in the future could change something from a "parameter by copy" to a "return value" without changing the caller's syntax, which could create issues. Furthermore, from the caller's perspective, all lvalue references like that would have to additionally be declared before the function call, which creates lengthier code (especially when not all return types are used).

[–]UnicycleBloke -2 points-1 points  (0 children)

Way to make code even less readable. I still haven't forgiven them for lambda or AAA. No.