ISO/IEC JTC1 SC22 WG21 N3897 - 2014-01-20
Ville Voutilainen, ville.voutilainen@gmail.comIn this std-proposals message, Faisal Vali presented his prototype proposal for "auto NDSMIs". He provided the following summary:
Essentially, things you can NOT do in the auto NSDMI:
- use sizeof/alignof on the class being defined
- create an object of the type being defined
- refer to any auto members in in-class static member initializers (class is not complete in them)
- refer to one auto member before its lexical definition in another auto member's initializer, or in areas where the class is not marked-complete (decltype within member function return types).
- refer to any member functions with deduced return types (although in clang you can not do this in non-auto members currently - is this a bug or do we intend to support:
struct X { auto f() { return 0; } int i = f(); };
Stuff you can do as long as you don't violate the above:
- refer to other members/mem_funs defined before or after the auto member
- capture 'this' in class-level lambdas.
For e.g. the following works:
struct X { //expected-note{{implicit default constructor}} auto a1 = sizeof(this); auto& self = *this; auto a2 = this->mem; //expected-warning {{uninitialized}} auto *a3 = this; auto a4 = a3; auto a5 = mem_fun(); // double auto a6 = const_cast<const X*>(this)->mem_fun(); // char auto L = [this, &r = *this, p = this] (auto a) { return r(a, this->self, p->a2); }; auto a7 = ([=](auto a) { return a->a1 + a1; })(this); auto L2 = [=](auto a) { return self(a, *this, 3); }; int mem = 5; double mem_fun() { return 3.14; } char mem_fun() const { return 'a'; } template<class T> bool operator()(T t, X&, int) { return true; } };
Mike Miller voiced concern about the restrictions:
The concern I've always had about this is that there are lots of things you can't do in a non-static data member initializer for a member with an "auto" type, as reflected in the list in Faisal's message. My personal opinion is that the utility does not outweigh the difficulty in specifying and teaching the feature and avoiding the "can't do that" holes when using it.
John Spicer voiced similar concerns (note: for people who have followed the discussion, not everything in this summary paper is in strict chronological discussion-message order):
I have to say that the more this is discussed, the more I think we should not make a change to allow this use of auto. It introduces a significant set of special cases, which require a lot more to understand than the simple prohibition of auto in that context. It also changes the rules for when NSDMIs are instantiated in class templates, which seems like an unfortunate interaction. It seems like a lot of added complexity for users for what is, in my opinion, a minimal benefit.
Daveed Vandevoorde explained some of the difficulties of the feature in general:
My main problem with auto non-static data members is that the meaning of initializers is subtly different in them. I.e., if I replace "auto" by the type that would be deduced, the meaning of the initializer can change (e.g., because it SFINAEs on completeness of the enclosing class).
Herb Sutter thought that we already have the problems pointed out, but the syntax for it isn't convenient:
As we've found before, decltype already opened the barn door to all of this -- as soon as we added decltype (and a couple of simple modifiers like remove_reference et al.), we automatically also added the ability for the user to express all possible uses of 'auto' type deduction -- whether we actually allowed the simple 'auto' spelling for a particular case (yet) or not.
Daveed elaborated further with an example:
No, in:<expr1> and <expr2> are evaluated in different contexts (<expr2> in the completed class; <expr1> not).struct S { // ... decltype(<expr1>) x = <expr2>; };
Daveed continued with answers to examples given by Herb:
The f and the g selected on both sides of the "=" may be different.remove_reference<decltype( f(42) + g("foo") )>::type m1 = f(42) + g("foo");
auto m1 = f(42) + g("foo");
Bad scenario #1: The authors of f and g decide to change their return type (or you pick up a new and better overload candidate), and now your class has changed layout. If you're lucky you'll notice before it blows up.
Bad scenario #2: After some time the author of X realizes that it's clearer (and safer, see scenario #1) to write the actual type of m1. Unfortunately, f and/or g are clever SFINAE templates that happen to depend on the completeness of X (not likely in this very small example, but still possible; and more likely with other expression forms and class member collections).
Herb asked, as a summary question:
So whatever complex rules we're worried about exists already, doesn't it?
Daveed summarized:
They don't exist for "auto". Today, if I replace "auto" by the type that would be deduced, I pretty much just documented the expected type. With auto nonstatic data members (ANDMs?) that's no longer necessarily true: I might change the semantics altogether.
Bjarne Stroustrup pointed out an ODR concern:
My main concern is that the feature will be popular and affect linkage in ways that are surprising to users (not surprising to language lawyers - it will be obvious what is going on from the rules). Note that ODR violations from inconsistent member types are not required to be diagnosed (or so I assume).
Ville Voutilainen pointed out the uses of the facility in general, uses that are harder to write with current language facilities:
Perhaps a variation of (2) is
- lambdas as class members
- somewhat python-esque programming where the NSDMI is the interesting part, not necessarily the resulting type (I don't have good practical examples where that is beneficial, but
struct X { auto x = make_shared<Foo>(bar); };might appeal to some)- nifty wrappers,
template <class T> struct X { auto x = T().foo(); };
struct X { auto x = "data"_with_a_udl; auto y = "data"_with_another_udl; };
Faisal summarized his view of the outcome:
The more I think about this, the more I agree with Bjarne & John. Discussion/analysis about the intended benefits and convincing use-cases for this feature are still somewhat lacking (at least in recent record) - and is clearly neccessary before we can gauge the appropriateness of the complexity-tax that it might/should incur on the users - so FWIW, here are my thoughts.
Like Ville, I feel enabling lambdas as class members is probably one of the more interesting aspects of this feature - although I admit a compelling use case eludes me right now - and this may qualify it as a feature in search of employment (perhaps there is a mighty library-writer out there who is looking to hire (or fire ;) I sense there is something important here - but I can't put my finger on it. It is of course worth noting that in C++14 users have the option of returning lambdas from member functions with deduced return types - so with appropriate optimizations in place, one might be able to reasonably approximate the aforementioned functionality.
In regards to having to spell a horribly named type when it's obvious from the context (perhaps the raison detre for auto and generic lambdas), when declaring a data member: - this can indeed be a real nuisance at times; especially, when one decides to rename typedefs post code review - it would be nice to have a more elegant solution for this (even if it's not spelled auto) - obviously refactoring tools can go a long way in mitigating this problem.
As for keeping member types automatically synchronized with the type of the initializer and avoiding implicit conversions - this also seems useful (should one need it) - and while my understanding of this use-case space is limited, I sense the space might overlap with that of the above (i.e. avoiding having to spell the typename) ...
I guess I could see the value of the following - but I fear it might not be terribly convincing:
struct Comparator { auto InternalFunctor = get_my_comparator(); use internalfunctor ... do i care if it is ever a ptr-to-fun, std::function or a stateful/stateless lambda or another invokable object? };
Also, if we should ever decide to have concept constrained data members - it might be useful to gain experience with a degenerate case (auto, auto*, auto&) - but I do not know of good use cases here either.
Now, concerning technicalities and usability of the feature: In regards to most (all?) of the limitations of auto members, compared to non-auto members - the price that is paid for violating those rules - at least in the unofficial-clang implementation is that a diagnostic error is issued and the user might learn and perhaps decide to explicitly name the type. Of course, they may still fail the C++ pub quizzes offered at the ACCU conference ;)
The ODR violation issue is of course the silent killer - there is no doubt that the potential for ODR violations is increased - 'auto' members may go viral and hijack the DNA of the class ;) - but like others I wonder how much this increases the mass of the current galaxy of ODR violation permutations. Perhaps the field of analytic combinatorics might shed some light (if anyone is bored enough to perform rigorous mathematical analysis of the various combinations and create models of "average" c++ programs ;)
[As another example, this template instantiated with the same arguments (of non-builtin types) can result in silent ODR issues depending on what is included before it:
template<class T> struct X { decltype(f{T{}) a = f(T{}); };
]
Having said all of that - since the number of committee wizards who have voiced serious concerns about this feature outnumbers the dwindling few who have voiced a strong interest in this feature - unless the winds of opinion undergo a dramatic change - it is my view that the authors of any future paper on this topic might do better to spend there time elsewhere.