First class tags for named constructors and much more

273 views
Skip to first unread message

mihailn...@gmail.com

unread,
Jul 16, 2018, 8:25:24 AM7/16/18
to ISO C++ Standard - Future Proposals

To recap the problem itself, here is the c++FAQ entry about it http://www.cs.technion.ac.il/users/yechiel/c++-faq/named-ctor-idiom.html


Sidenote. Some will argue the problem is most correctly solved by introducing new type(s).  
In that particular case this might be true, but that is not always the case - often a new type is not the best solution,  
either because the type would be completely superficial or because it is not feasible to introduce and maintain dozens of types, not used outside a limited context.
 

Part 1: Intro

This topic suggests building upon the tag idiom, already used throughout std, to enable "named constructors" by a way of "label arguments"

Here are the improvements over the idiom
  • provide a trivial way to create tag arguments (label arguments)
  • provide out-of-the-box way for the labels not to interfere with the rest of the types and the variables
  • provide ways for the labels to "just work" on the call site with the least qualification possible
  • provide syntax for the labels to be visually district from regular arguments (which gives invaluable semantic context to the reader of the code)
  • provide a way the labels to look the same on the declaration and on the call site (WYSIWYG)
If these points are done right, in a way, we have an enabling technology - a better way to create interfaces. 
interfaces that are not less expressive then named functions, while still working well with other key language features like constructors and template metaprogramming.

Without further ado, lets solve the problem c++faq presented using label arguments.

class Point 
{
public:
  case struct cartesian;
  case struct polar;

  Point(case cartesian, float x, float y) {. . .}
  Point(case polar, float r, float a) {. . .}
};

int main()
{
  Point p1 = Point(cartesian: 5.7, 1.2); 
  Point p2 = Point(polar: 5.7, 1.2); 

  return 0;
}

That's it.

What do we gain over the static functions? Quite a lot actually
  • No code refactor to transform a constructor to a static function. (no knowledge of static functions for that matter)
  • Very similar, yet expressive, syntax on the call side
  • All the benefits of using a constructor:
    •  Use all the tools that expect a constructor - heap allocation with new , make_*,call construct etc
    •  Use all the tools metaprogramming gives like SFINAE. 
    •  Self-explanatory what actually creates the object, no documentation or convention needed.
    •  Take advantage of template deduction guides if necessary.
Each one of these is a considerable win, no matter how one looks at it!
It is no wonder the tag idiom is adopted in the standard library, lets try to implement the above using it.

class Point
{
public:
  struct labels //< or a namespace outside class
  {
    struct cartesian_t{  explicit cartesian_t() = default; };
    struct polar_t{  explicit polar_t() = default; };
     
    inline static constexpr auto cartesian = cartesian_t();
    inline static constexpr auto polar = polar_t();
  };
   
  Point(labels::cartesian_t, float x, float y) {}
  Point(labels::polar_t, float r, float a) {}
};

int main()
{
  Point p1 = Point(Point::labels::cartesian, 5.7, 1.2);
  Point p2 = Point(Point::labels::polar, 5.7, 1.2);
       
  return 0;
}


Not that bad, or? 
Here are the list of problem with this "by hand" solution

- First -

Experts only! On the call site it might be "fine", but no junior to intermediate programmer will dare implementing this interface of its own objects. 

Dummy class as scope, empty structs, explicit, default, inline, static - we used half of the keywords in C++ together with non-obvious idioms like empty classes.

At what stage one teaches this to students?
At what stage one recommends this? 
I know the answer -  only if the staff you want to do is even more complex! 

But, our case is trivial! This leaves the only practical approach for non-experts to be static functions with all their downsides. 

Yet, even for experts, the situation is far from ideal.

- Second -

There is no association b/w the type present in the declaration and the object used on the call site - We have to use a convention.
The problem with conventions is, they can't be reinforced, and indeed they are broken or even non-existent - std::sequenced_policy does not indicate in any way std::execution::par can be used on the call site.

To use the interface one must read the documentation


- Third -

There is no way to tell an argument is a tag and not a normal, user-side object.

To use the interface one must read the documentation


- Forth -

Because these are regular types, we must go through hoops to protect ageist name collisions both for the type and the object.

This is crucial if we want any form of natural naming on the tags!

For example

class circle;
//...
shape(labels::circle_t, . . .); //< object is 'circle'

Here circle is good first choice for both a label object name and a concrete type.

However, if we use qualified names, we lose natural naming almost by definition. 
It is a lose-lose situation - we go the extra mile to protect our names, and yet if there is a collision, we are not able to use them naturally.


- Fifth -

Even with no collision we are forced to use qualified names in many cases as the code would be impossible for a human to parse correctly otherwise.

rect(point pos, size sz);
rect(labels::center_t, point val, size sz={10,10});

// user

rect(center, {10,10}); //< which constructor is called? No way to tell without tracking 'center' down 
                       // to see if it is a tag object or the name of some variable of type point

Of course, the alternative is to use "smart naming". 

An example from std:

std::allocator_arg

Hardly the best option. We will return to this example again later.


- Sixth -

labels is just a convention, and as with all conventions, it will be broken or not be present at all.

If that's not bad enough, we still risk collision this time with the scope name itself.


Now, before we dive into details, lets see some more examples

case struct center;
case struct bottomLeft;

struct circle
{
  circle(point p, size sz);
  circle(case bottomLeft, point p, size sz);
};

case struct circle;
case struct rect;

struct shape
{
  template<class... Args>
  shape(case circle, Args&&...); //< "in-place" construct a circle as shape
  shape(circle, color);  //< from object of type circle

  template<class... Args>
  shape(case rect, Args&&...); //< "in-place" overload for rect
};

// usage highlights

const auto circle = mycircle();
shape(circle, red); //< as usual, from object of type circle

shape(circle: bottomLeft: p, sz); //< calling implicit "named constructor" for circle - impossible with static functions!

auto sh = std::make_shared<shape>(circle: p, sz); //< yep, it works - the label is not part of the name of the function!

Also note, we can build any shape using SFINAE.


The usefulness of this idiom is by no means limited to constructors.

We can imagine

write(Data);
write(case noCommit, Data);

This is more then valuable alternative to any of the current methods - a bool argument, an enum argument or a different function name. 
For a real word example one should look no further then parallel stl.

Lets return to constructors

Label arguments are transparent to the type system once bound to a typename.
As a result template argument deduction just works, again, something impossible with static functions. 

template<class T>
struct rect
{
  template<LabelArg StartPoint, Point2D Pos, Size2D Size>
  rect(StartPoint, Pos, Size) {}
  T x,y,w,h;
}

template<LabelArg StartPoint, Point2D Pos, Size2D Size>
rect(StartPoint, Pos, Size) -> rect<best_type_t<StartPoint, storage_t<Pos>, storage_t<Size>>>; 

// user

rect(center: floatPoint{}, intSize{12,12}); //< rect<best_type>


Lastly, lets  look at std::allocator_arg again. 
With proper tag support inviting new "descriptive" names is not needed

namespace std {
case struct allocator;
}

// user 

using namespace std; //< just to highlight the lack of collision
                     // note that it is incorrect as the user must be able to tell, a std::allocator label is required. 

template<Allocator allocator>
object(. . ., case allocator, allocator alloc);

// invoke

object(. . ., std::allocator: MyAllocator{});


Part 2: Details

Lets break it all down.


Labels are declared-and-defined simply by prepending case to (possibly templated) class or struct declaration. 

case struct center;

This will create roughly the equivalent code, in the enclosing scope  

label_scope //< name is just for illustration, the compiler picks the name
{
  struct center { /*explicit ctor, spaceship or whatever*/  };
  inline static constexpr auto center_v = center(); 
}

label_scope is implementation defined, but it should probably be either a class, if the enclosing scope is a class, or a namespace, if the enclosing scope is a namespace.

In any case this scope should never collide with any other namespace (or class) even if this means a separate name for every TU. The name will never be visible and it is purely implementation detail.

At this point we have a type and a constant that are invisible for the rest of the typesystem, which is, as we saw, what we need.

To bring the type into scope, one must use case before its name. This will prepend the hidden scope name to the type name.

case center == label_scope::center

case std::allocator == std::label_scope::allocator

Used in this way, the label becomes a regular type name and can be used in any place type is required 

void (*)(case center, point, size);
std::is_same<case center, case foo::center>_v;

constexpr auto c = case circle{};
using T = case circle; // or even "using circle = case circle;"

Other then the type, we also need the constant. 

Although we could simply create instances of the label type, this is far from perfect and not what the established practice is currently.

To get the constant we append colon (:) to its name.

This will first append the hidden scope, then instead of the type, it will fetch the constant:

circle: == label_scope::circle_v

However, because the need for the constant is limited to only 3 cases -  a function call, to take its address and to declare it as default argument, 
I suggest to allow the syntax only in the first case - to call a function. 

Default labels will always look awkward, besides, one can still construct{} it. 
Taking the address, for whatever reason, can be done using std::addressof, which itself is a function.

This restriction prevents us from collision with any C labels and from funky code like &circle:;

The syntax case label: is not allowed as it is not needed and makes it easier to prevent any collision with switch statement labels.

There are two more pieces left. 

The first one, is to allow the comma after a label to be omitted.
This means that both of these are the same

shape(circle:, r, a);

shape(circle: r, a);

This "quirk" can be something positive as this will aid teaching the fact, labels are just arguments. 
Not only that, but some might as well preferer using the comma for all argument passing.
Lastly, if we are to ever have named arguments this can be used to be extra clear, we are passing a label argument, not naming a regular argument.

In any case, the colon is vital for reading code in the correct context, not unlike human language.

Second, and more importantly, we introduce special rules to deduce unqualified, or partially qualified, label names.

The reason for this is before everything else a semantic one:

Normal arguments are user-created object, they live predominately in user code and are foreign to the function.
Label arguments, quite the opposite, they are as much as part of the function as its name (semantically, not technically) - they, 
in the overhelping majority in cases, do not live in user code.

The only exception is specializing for the stl (like std::allocator) or specializing the stl itself (like introducing new execution policies).
We saw that the first case works as expected - one just passes the std-provided label object to mark the custom allocator. 
The second case is a bit more tricky and might force the user to do an explicit call to the label argument to prevent the std one being picked by default.
This can be trivially resolved by providing a user-facing interface in non-std namespace, which delegates the call to the correctly qualified std function

With that in mind there is no reason to apply standard lookup rules to functions, called with a label argument. 
Instead of looking for the function in the namespaces of the label argument, we look for the label argument in the enclosing scope of the function.

This will allow us to have natural label expressions, without the need of using in the enclosing namespaces or the need to qualify the labels. 

Point p1 = Point(cartesian: 5.7, 1.2); //< label_scope::cartesian looked in the enclosing scope of Point::Point(. . .), which is the Point class

A hypothetical

namespace std {
case struct in_place;
template<class... Args >
optional(case in_place, Args&&... args );
}

// user

case struct in_place;

void func() 
{
  std::optional<string>(in_place: 3, 'A'); //< in_place in std used 
// std::optional<string>(::in_place: 3, 'A'); //< ::in_place used
}

Another example

struct circle
{
  case struct bottomLeft;
  ...
  circle(case bottomLeft, point p, size sz);
};

using std::optional;
optional<circle>(in_place: bottomLeft: p, sz); //< in_place still in std, bottomLeft in circle

Lookup like this might be easer said then done, there is no denying however, it is the more correct one in general - where the label is located depends on the function, not the other way around.

It is hard to tell if such a lookup will make finding the function itself harder or easer - after all, the fact that the compiler knows the argument is label one, dramatically can limit the overlanding set,  
no matter in which namespace is either the function or the label argument. In a way, all labels are as if a subclass of just one parent class, culling based on that should help lookup.  


Part 3: Status Quo

Sadly, we can't make current standard types, used as tags, "just work" as label arguments, not without introducing completely new, parallel rules. 
Even if we do, the results will be partial - the dissonance b/w declaration and invocation will still be present. 
We also can't silently change the lookup rules without breaking user code, the best we can do is allow colon as separator of "tag classes" (empty classes with explicit ctor),
but this, is as I said, completely parallel rule with questionable benefits.

It should be noted, migrating the stl to labels is trivial - it literally means two lines of code for every function using a tag. One to introduce a case struct, reusing the old name, and one to create a forwarding overload.
Doing this will give us all the benefits of using labels - association b/w declaration and invocation, better lookup rules and more expressive syntax.


Beyond that we could rethink the naming variations of some standard functions. The name of the function is to describe what the function does
if we are forced to name the function based on what the arguments are (to make it more clear), or invent names to avoid redeclaration, then we have limitation of the function declaration syntax.
This proposal is not well suited for the first case, but is quite well suited for the latter.

For example, std::find is descriptive to anyone, including a non-programmer, however find_if - not so much, if at all.

By naming the entire function this way it is implied the function itself have some sort of prerequisite of to when and if the finding process takes place - "perform finding (only) if something, otherwise - bail out".
However by simply shifting the "if" part to the comparison function we radically improve the communication with the user.

std::find(begin, end, if: [value](auto val){. . .});

On one hand we keep the name intact - find still does exactly the same as before, no need for a name change, how it does the search has changed however, and we have the opportunity to specify this naturally, as an argument.
On the other hand, by moving if right next to the predicate it is immediately clear - "if this returns true, we found our match"

It is arguably the single most clear interface possible, and it is all about human language.


Part 4: Conclusion

Presented here was a method for introducing human (and templates) friendly functions, that are easier to define then static functions, when used as "named constructor", 
and at the same time are more expressive, even for normal functions, as the "naming" can be done in-between arguments.
The method is also as powerful as the current tag idiom, used in generic code, yet much easier (and more clear) in both declaration and usage.

By having these qualities this feature has the potential to combine the two idioms into one core feature, usable across the entire spectrum of C++ programmers expertise.


Thank You for Your time!

Zhihao Yuan

unread,
Jul 16, 2018, 10:43:48 AM7/16/18
to std-pr...@isocpp.org

This call site syntax looks like saying the first argument
has value 5.7

Besides, regardless of what can be done for generic
code, “named constructors” should at least support
the Point::cartesian(5.7, 1.2) syntax.

--
Zhihao Yuan, ID lichray
The best way to predict the future is to invent it.
_______________________________________________

mihailn...@gmail.com

unread,
Jul 16, 2018, 2:54:21 PM7/16/18
to ISO C++ Standard - Future Proposals, z...@miator.net


On Monday, July 16, 2018 at 5:43:48 PM UTC+3, Zhihao Yuan wrote:

This call site syntax looks like saying the first argument
has value 5.7

 
Few ways around this. 

First and foremost, people will get used to the multiple use of label arguments - 
in one case they label the function itself, like in parallel stl cases, 
in other they label an argument, like the rect(bottomLeft: point, size)case 
and often they label multiple arguments like the case here and all "in-place"-like constructors in std.

The designer is also free to accept std::pair instead, forcing a more explicit call - Point(cartesian: {5.7, 1.2});
or, in more complex cases, he can add another label to break parameter "packs".

Lastly, one is free to use a comma after the label - Point(cartesian:, 5.7, 1.2); so to be more specific.
 

And don't forget Point::cartesian() is not any better! Not by a long shot.
If this is to be done right it should be following a convention like the Qt's "from" one  - fromCartesian(. . .)- just a floating static function with the name 'cartesian' means little without documentation. 
At least as label the user can deduce, it is not about the first variable as "a cartesian float" does not mean anything, neither does "polar float". 

Also, in reality "cartesian" should be implicit. The user will ever have to specify only "polar: . . .", as it is unusual.
 


Besides, regardless of what can be done for generic
code, “named constructors” should at least support
the Point::cartesian(5.7, 1.2) syntax.



Static functions used as named constructors don't work with template argument deduction (along with many other downsides) and we should move away from them completely
The fact we teach and use this idiom for 30 years does not make it a good one - a static function in no way implies construction.

Static functions have no benefits besides written text to the user, something label arguments are capable of doing, not only that, but the text can move around the arguments.

Yes the user will need to learn to read labels, but chances are, one will be to deduce the meaning from context, much like one can, most of the time, deduce which static functions are "constructors".

Richard Hodges

unread,
Jul 16, 2018, 3:19:43 PM7/16/18
to std-pr...@isocpp.org
On Mon, 16 Jul 2018 at 20:54, <mihailn...@gmail.com> wrote:


On Monday, July 16, 2018 at 5:43:48 PM UTC+3, Zhihao Yuan wrote:

This call site syntax looks like saying the first argument
has value 5.7

 
Few ways around this. 

First and foremost, people will get used to the multiple use of label arguments - 
in one case they label the function itself, like in parallel stl cases, 
in other they label an argument, like the rect(bottomLeft: point, size)case 
and often they label multiple arguments like the case here and all "in-place"-like constructors in std.

The designer is also free to accept std::pair instead, forcing a more explicit call - Point(cartesian: {5.7, 1.2});
or, in more complex cases, he can add another label to break parameter "packs".

Lastly, one is free to use a comma after the label - Point(cartesian:, 5.7, 1.2); so to be more specific.
 

And don't forget Point::cartesian() is not any better! Not by a long shot.
If this is to be done right it should be following a convention like the Qt's "from" one  - fromCartesian(. . .)- just a floating static function with the name 'cartesian' means little without documentation. 
At least as label the user can deduce, it is not about the first variable as "a cartesian float" does not mean anything, neither does "polar float". 

Also, in reality "cartesian" should be implicit. The user will ever have to specify only "polar: . . .", as it is unusual.
 


Besides, regardless of what can be done for generic
code, “named constructors” should at least support
the Point::cartesian(5.7, 1.2) syntax.



Static functions used as named constructors don't work with template argument deduction (along with many other downsides) and we should move away from them completely
The fact we teach and use this idiom for 30 years does not make it a good one - a static function in no way implies construction.

Agree
 

Static functions have no benefits besides written text to the user, something label arguments are capable of doing, not only that, but the text can move around the arguments.

Yes the user will need to learn to read labels, but chances are, one will be to deduce the meaning from context, much like one can, most of the time, deduce which static functions are "constructors".

 


--
Zhihao Yuan, ID lichray
The best way to predict the future is to invent it.
_______________________________________________

 

From: mihailn...@gmail.com <mihailn...@gmail.com>
Sent: Monday, July 16, 2018 7:25 AM

  Point p1 = Point(cartesian: 5.7, 1.2); 

  Point p2 = Point(polar: 5.7, 1.2); 


There's a problem with this motivating example.

A cartesian co-ordinate cannot be converted to a polar co-ordinate without providing the conventions implicit in the world geometry. 

For example, is the angle clockwise or anti-clockwise and which axis corresponds to zero degrees of rotation?

You may want to consider:

class Point2d;
class Polar2dCoordinate;
class Geometry2d;

Point2d::Point2d(Polar2dCoordinate const&, Geometry2d const&);
Polar2dCoordinate::Polar2dCoordinate(Point2d const&, Geometry2d const&);

Or similar conversion functions.

This has the advantage of being self-documenting doesn't it?
 

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/cfef3371-ce08-4e72-97ad-4328333be859%40isocpp.org.

cpplj...@gmail.com

unread,
Jul 16, 2018, 5:45:20 PM7/16/18
to ISO C++ Standard - Future Proposals, mihailn...@gmail.com
[snip]
 A small bit of bikeshedding: Why violate DRY {Don't Repeat Yourself}
by repeating case and struct?  Why not, instead, use:

public:
     case
     { cartesian
     , polar
     };
?

Zhihao Yuan

unread,
Jul 16, 2018, 6:11:35 PM7/16/18
to mihailn...@gmail.com, ISO C++ Standard - Future Proposals

I had never said that this syntax is static functions, I only
said that your feature should be able to be used with that
syntax.

mihailn...@gmail.com

unread,
Jul 17, 2018, 2:30:07 PM7/17/18
to ISO C++ Standard - Future Proposals
Richard, I agree with you in general, but that is the example c++faq gives and I wanted to just compare and contrast.
They just included a minimal, nalive if you will, dummy system, just for example.

In any case, types are the right solution for this example on multiple occasions. 
Even if we keep their scenario, 'angle' can be a different type as it is not assignable to a coordinate like x and y.


About cpplj... suggestion

I actually like the idea for reasons, completely different - this makes it clear we are introducing a scope, and that is a good thing. 

Having said that, this will rise issues of its own - is this really a namespace, if yes, they why only one type of thing can be declared there and why it can be placed inside a class
if not, then why it can be reopened and behave pretty much like namespace in all other cases; should be called "case namespace"?
I am not against the idea, and I will suggest it as an alternative/future improvement, I simply want to be as conservative as possible at the moment. 


Zhihao, I am afraid, don't see how the two are connected and how can we reinforce that connection. Which label should be callable as function name? Should this apply to functions as well.
Why are we obligated to provide such alternative? What do we gain?


Richard Hodges

unread,
Jul 19, 2018, 4:57:48 AM7/19/18
to std-pr...@isocpp.org
Sorry for the top post. Perhaps the group could let me know if this is worthy of a separate thread.

I actually have a use case in which areas of this proposal would be useful.

I am writing a library which wraps c-style libraries into c++ value-type objects.

I have a class which acts as an indication that a raw pointer is being adopted:

namespace wrapper { namespace core {

template<class T>
struct adopted_pointer
{
adopted_pointer(T *p) noexcept
: ptr_(p)
{
}

adopted_pointer(adopted_pointer const& other) = delete;

constexpr adopted_pointer(adopted_pointer&& other) noexcept
: ptr_(other.ptr_)
{
other.ptr_ = nullptr;
}

adopted_pointer& operator =(adopted_pointer&& other) = delete;

adopted_pointer& operator =(adopted_pointer const&& other) = delete;

~adopted_pointer() noexcept
{
assert(!ptr_);
}

T *get()&&
{
auto result = ptr_;
ptr_ = nullptr;
return result;
}

T *ptr_;
};

There is further function in the wrapper namespace to create such an adoption indicator:

namespace wrapper {

using ::wrapper::core::adopted_pointer;

template<class T>
auto adopt(T *p) -> adopted_pointer<T>
{
return {p};
}

}

I can now specify explicitly in a constructor that a pointer created in another library is being adopted (i.e. we are controlling ownership):

auto ps = reinterpret_cast<char*>(std::malloc(100));
std::strcpy(ps, "hello, world");
auto ds = wrapper::core::dynamic_c_string(wrapper::adopt(ps));

The above code is in the global namespace, hence the need to fully qualify the call to adopt.

What might be nice is to be able to specify:

auto ds = wrapper::core::dynamic_c_string(adopt: ps);

Which in effect means 'search for the name 'adopt' in the callee's namespace rather than the caller's.

I think there is a reasonable argument for exploring this use case. 

R

mihailn...@gmail.com

unread,
Jul 19, 2018, 11:58:31 AM7/19/18
to ISO C++ Standard - Future Proposals
Great example! Also, the presence of adopt() is not evident without documentation, where using a label the interface is self-explanatory. 

BTW the Intrusive Smart Pointer proposal is in similar situation - they need to either construct or construct-and-retain. 
The initial code from 15+ years ago used a bool, they now use a tag.

explicit retain_ptr(pointer) noexcept;
retain_ptr(pointer, retain_t) noexcept(/* see below */);


Needless to say, on the call site a label argument will look better, if not great object(retain: obj)

Nicol Bolas

unread,
Jul 19, 2018, 12:00:51 PM7/19/18
to ISO C++ Standard - Future Proposals
I mentioned something to this effect on another thread. It was essentially the ability to deduce the full "pathname" of an identifier from the location where that identifier gets used. Such syntax would be useful in a number of places.

Switching over an `enum class` is a place where people often find it annoying to have to re-type `EnumName::` before the enumerators, even though the compiler knows statically where the enumerator comes from. Now obviously you can't use `identifier:` for that syntax, since `case` statements are terminated by `:` characters. But the principle is meaningful.

The problem of course is that this is somewhat orthogonal to what this proposal is trying to achieve. That is, this proposal does do full "pathname" search of these types, but only as a consequence of its far more specific goal. As I said in the other thread, it's like Gor-coroutines vs. Core-coroutines: policy vs. mechanisms. Policies can only be used for one thing, while mechanisms can be used for a plethora of things.

Basically, with "pathname" identifier lookup syntax, you get get the effect of this proposal, except that you need to do `{},` after such tags. The OP seems to think that this is not good enough, that we really need to disguise the fact that an argument is an argument, so the more general mechanis is unlikely to be pursued by them.

Nicol Bolas

unread,
Jul 19, 2018, 12:09:23 PM7/19/18
to ISO C++ Standard - Future Proposals
On Thursday, July 19, 2018 at 12:00:51 PM UTC-4, Nicol Bolas wrote:
Basically, with "pathname" identifier lookup syntax, you get get the effect of this proposal, except that you need to do `{},` after such tags. The OP seems to think that this is not good enough, that we really need to disguise the fact that an argument is an argument, so the more general mechanis is unlikely to be pursued by them.

I should correct this. Because of inline variables, such syntax would not even need `{}`. So armed with such syntax (which we can debate), you would be able to do this, where `adopt` is an inline variable:

auto ds = wrapper::core::dynamic_c_string(adopt:, ps);

as opposed to the OP's idea of this:

auto ds = wrapper::core::dynamic_c_string(adopt: ps);

That is, with the OP's idea, you get to drop a comma. With mine, you have to use a comma and a variable template, but you solve a bunch of other problems too.

mihailn...@gmail.com

unread,
Jul 19, 2018, 2:30:43 PM7/19/18
to ISO C++ Standard - Future Proposals
Well, you don't have to drop the comma, the first is still valid. The colon is just to pick the fully qualified variable.

Arguably, this could work without the colon, just by a virtue of being a label type. 



And I guess the Enum case you mean is like this

enum class A{ yes, no };
enum class B{ yes, no };

func(A, int);

// use

func(yes, 2); //< works

I am worried, people will feel, we are going back,
but that aside, enums already contribute to ADL, and this will complicate things, unless we use new syntax like :yes
This will tell the compiler to not find the argument first, but last, after the function, then he will be able to prepend A.

So I guess this could extend to other case, but it either needs syntax or, not sure if possible, some sort of template specialization - can template specializations control such things, I doubt it. 


However note this is not the same as with label arguments. This one is looking in the scope of the argument (enums, classes), labels look in the scope of the function itself.

To make it also look in the function namespace first is de facto a different rule. 



mihailn...@gmail.com

unread,
Jul 20, 2018, 4:21:16 PM7/20/18
to ISO C++ Standard - Future Proposals


On Thursday, July 19, 2018 at 7:00:51 PM UTC+3, Nicol Bolas wrote:


On Thursday, July 19, 2018 at 4:57:48 AM UTC-4, Richard Hodges wrote:
Sorry for the top post. Perhaps the group could let me know if this is worthy of a separate thread.
...

I mentioned something to this effect on another thread. It was essentially the ability to deduce the full "pathname" of an identifier from the location where that identifier gets used. Such syntax would be useful in a number of places.

Switching over an `enum class` is a place where people often find it annoying to have to re-type `EnumName::` before the enumerators, even though the compiler knows statically where the enumerator comes from. Now obviously you can't use `identifier:` for that syntax, since `case` statements are terminated by `:` characters. But the principle is meaningful.


Just saw this: Using Enum

Richard Hodges

unread,
Jul 20, 2018, 5:10:53 PM7/20/18
to std-pr...@isocpp.org
Hmm. How about:

auto ds = wrapper::core::dyanmic_c_string(.adopt=ps);

Which logically would be a synonym for:

auto ds = wrapper::core::dyanmic_c_string(.adopt(ps));


I don't think this syntax conflicts with any other. The idea being that the leading "." implies "current context, or current namespace". It's akin to the old convenient VB syntax of:

with foo
  .a = b
  .doSomething()
end with


It also starts to look like named arguments, which are a nice feature of (for example) python.

I'm wondering how you think this looks:

void test()
{
  auto p = geometry::point{.polar={.angle=1.4, .length=6.0}};
}

Given that this has already been defined:

namespace geometry { 

  struct cartesian
  {
    double x, y;
  };

  struct angle {
    angle(double rads);
    double rads_;
  };

  struct length {
    angle(double len);
    double len_;
  };

  struct polar
  {
    polar(angle, length);
    angle a;
    length r;
  };

  struct point
  {
    point(cartesian);
    point(polar);
  };
}

It's expressive, but is it too much?

 

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.

mihailn...@gmail.com

unread,
Jul 21, 2018, 11:38:57 AM7/21/18
to ISO C++ Standard - Future Proposals


On Saturday, July 21, 2018 at 12:10:53 AM UTC+3, Richard Hodges wrote:


On Thu, 19 Jul 2018 at 18:09, Nicol Bolas <jmck...@gmail.com> wrote:
On Thursday, July 19, 2018 at 12:00:51 PM UTC-4, Nicol Bolas wrote:
Basically, with "pathname" identifier lookup syntax, you get get the effect of this proposal, except that you need to do `{},` after such tags. The OP seems to think that this is not good enough, that we really need to disguise the fact that an argument is an argument, so the more general mechanis is unlikely to be pursued by them.

I should correct this. Because of inline variables, such syntax would not even need `{}`. So armed with such syntax (which we can debate), you would be able to do this, where `adopt` is an inline variable:

auto ds = wrapper::core::dynamic_c_string(adopt:, ps);

as opposed to the OP's idea of this:

auto ds = wrapper::core::dynamic_c_string(adopt: ps);

That is, with the OP's idea, you get to drop a comma. With mine, you have to use a comma and a variable template, but you solve a bunch of other problems too.

Hmm. How about:

auto ds = wrapper::core::dyanmic_c_string(.adopt=ps);

This way we "overload" assignment - sometimes it is real assignment, sometimes it is for labeling/function calling. 
As for the dot, the problem is in C++ scope is not a dot, but double colon, the dot is used only for member access, and in a way we overloaded that as well, semantically.
(That's why I suggested the single column prefix to donate "search in scope" - to be closer to the double colon.)

But as I said, with using namespace, inline namespace and now eventually using Enum I don't see the need a yet another way to skim on typing for general arguments.
The labels are special case only because they need to be perceived "as if" part of the function. 
I might be wrong, there might e benefits generalizing this approach, but fail to see real gain (contrarily to the labels)

Bengt Gustafsson

unread,
Jul 21, 2018, 6:49:41 PM7/21/18
to ISO C++ Standard - Future Proposals
I think you are mixing up type and parameter names. I assume that you mean that .polar at the call site is a type name, but this is contrary to the C named initializers which are member names (or here that would presumably mean parameter names).

The possibility of defining separate polar and cartesian types has already been mentioned and dismissed, so .polar at the call site must mean a parameter name, which means that you have to reformulate your geometry namespace contents. I think that this is the way to go, as the tag colon syntax is very novel and as the declaration has to prepare for call sites to be able to use the feature.

Proposing being able to use parameter names at call sites comes with the mandatory objection that different declarations of the same function signature may specify different parameter names. Currently this has no effect other than confusion on the part of the human reader. To make the example work it must be possible to overload on parameter names, which means that two different declarations declare different functions with the same set of types for their parameters. For old code that uses different names we get different types of errors with the new definion:

1. For methods being implemented out of line we get an error that the method is not declared in the class head.

2. For free functions we get two separate functions which can lead to unresolved external errors from the linker.

3. Old call sites which have seen two type-equal declarations with different parameter names and which do not mention any parameter names (i.e. all old call sites) an ambiguous call error will result.

4. The mangled names will definitely be changed which is an ABI breakage.

5. If the same function declaration has implementation in both a direct linked object file (with other parameter names than declaration) and a library (with same parameter names as declaration). Currently this selects the implementation from the object file and does not use the function in the library during linking, while in the future the implementation from the library will be used (as its mangled name matches).

Out of these I would be most concerned about 4, as there seems to be a strong wish to not break ABI compatibility. At some point, maybe for C++23, an ABI update may be desired as the backwards compatibility restrains development on the standard library side (as you can't change the object layout without breaking the linkability).

Number 1-3 are all loud errors and fairly easy to correct. 2 would benefit from some error message improvement by the IDE when it sees the linker error though.

5 should be very rare, but on the other hand it is a silent error. Then again this seems to be more likely to reveal pre-existing bugs than to introduce new ones as it is usually erroneous to implement the same function in multiple places. (The most common situation is probably that the functions are identical leftovers from some copy-paste operation anyway).

At this point I have not dug down into the rule updates that would be necessary when doing overload resolution for call sites with named parameters but I think it has been done before.

To sum up I am very skeptical to introducing a new type of names but I would be interested in a fresh look at named parameters. The main motivation is probably not the constructor case but the general improvement when calling function with many defaulted parameters, such as prevalent in GUI libraries and similar.

Can anyone pinpoint a language which sports both function overloading on type and named parameters at call sites (without help from declarations)? That would be an interesting source of inspiration rather than Python's untyped, unoverloadable functions.

Barry Revzin

unread,
Jul 22, 2018, 8:06:49 AM7/22/18
to ISO C++ Standard - Future Proposals


On Saturday, July 21, 2018 at 5:49:41 PM UTC-5, Bengt Gustafsson wrote:
Can anyone pinpoint a language which sports both function overloading on type and named parameters at call sites (without help from declarations)? That would be an interesting source of inspiration rather than Python's untyped, unoverloadable functions.

As far as I'm aware, this is valid Swift (it's not good swift, just valid Swift):

func add(a: Int, to b: Int) -> Int {
   
return a + b
}

func add
(a: Int, and b: Int) -> Int {
   
return a * b
}

let answer1
= add(2, to: 4)    // 6
let answer2
= add(2, and: 4)   // 8

 

mihailn...@gmail.com

unread,
Aug 2, 2018, 11:23:06 AM8/2/18
to ISO C++ Standard - Future Proposals, mihailn...@gmail.com
 

So let me tell you what I was thinking:

There exist a common, yet unutterable, where tag types live (lets call it tag_ns to simplify).

When you declare a function with tags:
int foo(tag_name: float f);
The compiler creates a tag type in this namespace called tag_name, and transform the declaration like that:
int foo(tag_ns::tag_name, float f);

When you call a function with a tag:
foo(tag_name: 1.f);
The compiler creates a tag type in this namespace called tag_name, replaces the expression with a call like that (without even needing to know the declaration of the function):
foo(tag_ns::tag_name{}, 1.f);

If two functions (even in different namespaces) use the same tag name, they will have the same tag type, which is fine.

Yes, initially I was thinking names are names no matter the namespace, but I abandoned this.

I abandoned it because tags are types, and are used for overload resolution. If the std has a tag named 'par' and I want to use this name but with different implementation, I must be able to do that, much like today
We need namespaces for tags, and,  to the very least labels should be defined in the namespace of the functions.

But then I also abended automatic introduction...
 
...

The main feature here would be the automatic introduction of types. A bit like a template instantiation. It is implicit.

Automatic introduction is not that good of a barging - we invent new rules to save some typing, but new problems arise. 

Where are tags created? How can we make them template? How can we add attributes to them? Is this really new signature?

Sidenote, how do you name the type to create a function signature? decltype(name:)? name: to mean different things in different contexts? Things escalate quickly. 

In the end, I made it all as conservative as possible and as much based on current practice as possible. The initial simplicity was deceiving, because it was just a morning idea.

This is not to say we could not use some shortcut syntax.

We could

int foo(tag_name: float f);

generate

case struct tag_name; //< enclosing scope
int foo(case tag_name, float f);

But I still question the pros/cons ratio. 




In your first proposal, you mentioned you wanted to be able to do something like that:
any(in_place<string>: "");
I don't think it is useful. you are not naming an argument here.

In most cases tags are used it is not as if the argument is named. 

Here it says "string arguments here:" and for that colon is grammatically correct.


Also not naming an argument, but still grammatically correct:

shared_lock( defer_lock: mtx); 
shared_lock( try_to_lock: mtx);
shared_lock( adopt_lock: mtx);

As in "...the lock of: [the mutex]"


Now, to be fair, you could do something equivalent in C++20:
int foo(std::tag<"tag_name">, float f);

foo
("tag_name"tag, 1.f);

But the shorthand syntax would be very nice.
Moreover, such a shorthand syntax would give people incentives to use it (I consider it a good thing).
Let's make a quick poll. Which one do you prefer? (try to see it as a newcomer to the language)
std::find_if(v.begin(), v.end(), functor);
std
::find(v.begin(), v.end(), "if"tag, functor);
std
::find(v.begin(), v.end(), if: functor);

I personally prefer the third one (I would like it even more with ranges).
This is not really about the possibility to do it now, but how the syntax looks.

Overall I want to clarify: that's my opinion, and I'm pretty sure some will disagree, but I really think it is worth pursuing. 

floria...@gmail.com

unread,
Aug 2, 2018, 11:58:01 AM8/2/18
to ISO C++ Standard - Future Proposals, mihailn...@gmail.com


Le jeudi 2 août 2018 17:23:06 UTC+2, mihailn...@gmail.com a écrit :
 

So let me tell you what I was thinking:

There exist a common, yet unutterable, where tag types live (lets call it tag_ns to simplify).

When you declare a function with tags:
int foo(tag_name: float f);
The compiler creates a tag type in this namespace called tag_name, and transform the declaration like that:
int foo(tag_ns::tag_name, float f);

When you call a function with a tag:
foo(tag_name: 1.f);
The compiler creates a tag type in this namespace called tag_name, replaces the expression with a call like that (without even needing to know the declaration of the function):
foo(tag_ns::tag_name{}, 1.f);

If two functions (even in different namespaces) use the same tag name, they will have the same tag type, which is fine.

Yes, initially I was thinking names are names no matter the namespace, but I abandoned this.

I abandoned it because tags are types, and are used for overload resolution. If the std has a tag named 'par' and I want to use this name but with different implementation, I must be able to do that, much like today
We need namespaces for tags, and,  to the very least labels should be defined in the namespace of the functions.

The tag has no implementation, only the overload using the tag has an implementation. So there is absolutely no problem here.
void foo(tag:);
void bar(tag:);

Why tag in both cases couldn't be the same? We use nothing from those tag objects.

How many real world code use non-empty classes as tags?
 

But then I also abended automatic introduction...
 
...

The main feature here would be the automatic introduction of types. A bit like a template instantiation. It is implicit.

Automatic introduction is not that good of a barging - we invent new rules to save some typing, but new problems arise. 

What are those problems ?
If tag: refers to a single type wherever you are (even across TUs), then what is the problem?
 

Where are tags created? How can we make them template? How can we add attributes to them? Is this really new signature?

First, I was thinking in an unutterable namespace, yet common to all TUs.
But then, making them an instantiation of std::tag<"tag_name"> might be better.

You don't add attributes to them because you don't need to. Do you add attributes to the definition of std::string?
 

Sidenote, how do you name the type to create a function signature? decltype(name:)? name: to mean different things in different contexts? Things escalate quickly. 

When you declare a function, you would do:
void foo(tag_name:);
// or
// void foo(std::tag<"tag_name">);

That's true name: would have different meaning depending on the context, but that's would be the case anyway because of labels.
And actually, when declaring a function and when using it, they don't mean the same language construct, but still have a common meaning: they are tags.
So only the translation would be different, not the meaning.

Actually, with decltype(name:), whatever the translation of name: would be (type or object of that type), the result is still the same.

 

In the end, I made it all as conservative as possible and as much based on current practice as possible. The initial simplicity was deceiving, because it was just a morning idea.

On the contrary, the initial simplicity was great. I think you shouldn't have tried to solve its issues by making more complex, but on the contrary making it more simple (like I did).
 

This is not to say we could not use some shortcut syntax.

We could

int foo(tag_name: float f);

generate

case struct tag_name; //< enclosing scope
int foo(case tag_name, float f);

But I still question the pros/cons ratio. 




In your first proposal, you mentioned you wanted to be able to do something like that:
any(in_place<string>: "");
I don't think it is useful. you are not naming an argument here.

In most cases tags are used it is not as if the argument is named. 

True, tags are not names.
Avoiding this use case makes the design very simple.
Much simpler than all the alternatives you tried.

mihailn...@gmail.com

unread,
Aug 2, 2018, 12:50:03 PM8/2/18
to ISO C++ Standard - Future Proposals, mihailn...@gmail.com, floria...@gmail.com


On Thursday, August 2, 2018 at 6:58:01 PM UTC+3, floria...@gmail.com wrote:


Le jeudi 2 août 2018 17:23:06 UTC+2, mihailn...@gmail.com a écrit :
 

So let me tell you what I was thinking:

There exist a common, yet unutterable, where tag types live (lets call it tag_ns to simplify).

When you declare a function with tags:
int foo(tag_name: float f);
The compiler creates a tag type in this namespace called tag_name, and transform the declaration like that:
int foo(tag_ns::tag_name, float f);

When you call a function with a tag:
foo(tag_name: 1.f);
The compiler creates a tag type in this namespace called tag_name, replaces the expression with a call like that (without even needing to know the declaration of the function):
foo(tag_ns::tag_name{}, 1.f);

If two functions (even in different namespaces) use the same tag name, they will have the same tag type, which is fine.

Yes, initially I was thinking names are names no matter the namespace, but I abandoned this.

I abandoned it because tags are types, and are used for overload resolution. If the std has a tag named 'par' and I want to use this name but with different implementation, I must be able to do that, much like today
We need namespaces for tags, and,  to the very least labels should be defined in the namespace of the functions.

 
The tag has no implementation, only the overload using the tag has an implementation. So there is absolutely no problem here.
void foo(tag:);
void bar(tag:);

Why tag in both cases couldn't be the same? We use nothing from those tag objects.

Tags are used for specialization of functions (parallel library is an example). I must be able to declare tags that are named the same as the std ones, yet are different type and will pick my overload, not the std one. 

If we completely remove namespaces for tags people will be forced to go the C-way of naming to be able to create new type under the "same name" - cuda_parallel - to force new overload.
 

How many real world code use non-empty classes as tags? 
 

But then I also abended automatic introduction...
 
...

The main feature here would be the automatic introduction of types. A bit like a template instantiation. It is implicit.

Automatic introduction is not that good of a barging - we invent new rules to save some typing, but new problems arise. 

What are those problems ?
If tag: refers to a single type wherever you are (even across TUs), then what is the problem?
 

Where are tags created? How can we make them template? How can we add attributes to them? Is this really new signature?

First, I was thinking in an unutterable namespace, yet common to all TUs.
But then, making them an instantiation of std::tag<"tag_name"> might be better.

You don't add attributes to them because you don't need to. Do you add attributes to the definition of std::string?
 

Sidenote, how do you name the type to create a function signature? decltype(name:)? name: to mean different things in different contexts? Things escalate quickly. 

 
When you declare a function, you would do:
void foo(tag_name:);
// or
// void foo(std::tag<"tag_name">);

That's true name: would have different meaning depending on the context, but that's would be the case anyway because of labels.

And what about std::function and pointers to functions? We will need to touch a lot of places to enable this syntax.  

Also note that we will need extra rules what introduces and what just names a label. And these are not trivial, we can't always blindly introduce. 
Makes no sense to introduce a type by declaring a function pointer, even if we agree to not have namespaces for them.

 
And actually, when declaring a function and when using it, they don't mean the same language construct, but still have a common meaning: they are tags.
So only the translation would be different, not the meaning.

Actually, with decltype(name:), whatever the translation of name: would be (type or object of that type), the result is still the same.

 

In the end, I made it all as conservative as possible and as much based on current practice as possible. The initial simplicity was deceiving, because it was just a morning idea.

On the contrary, the initial simplicity was great. I think you shouldn't have tried to solve its issues by making more complex, but on the contrary making it more simple (like I did).


It is more simple, in the sense, it maps 100% on to established practice and 95+% on current types, declarations, scope, etc, rules 

 
 

This is not to say we could not use some shortcut syntax.

We could

int foo(tag_name: float f);

generate

case struct tag_name; //< enclosing scope
int foo(case tag_name, float f);

But I still question the pros/cons ratio. 




In your first proposal, you mentioned you wanted to be able to do something like that:
any(in_place<string>: "");
I don't think it is useful. you are not naming an argument here.

In most cases tags are used it is not as if the argument is named. 

 
True, tags are not names.
Avoiding this use case makes the design very simple.
Much simpler than all the alternatives you tried.

Why should I avoid this case, I don't understand? The colon is perfectly deducible in different contexts to mean slightly different things, but it always stands for a clarification.

floria...@gmail.com

unread,
Aug 2, 2018, 4:12:10 PM8/2/18
to ISO C++ Standard - Future Proposals, mihailn...@gmail.com


Le jeudi 2 août 2018 18:50:03 UTC+2, mihailn...@gmail.com a écrit :


On Thursday, August 2, 2018 at 6:58:01 PM UTC+3, floria...@gmail.com wrote:


Le jeudi 2 août 2018 17:23:06 UTC+2, mihailn...@gmail.com a écrit :
 

So let me tell you what I was thinking:

There exist a common, yet unutterable, where tag types live (lets call it tag_ns to simplify).

When you declare a function with tags:
int foo(tag_name: float f);
The compiler creates a tag type in this namespace called tag_name, and transform the declaration like that:
int foo(tag_ns::tag_name, float f);

When you call a function with a tag:
foo(tag_name: 1.f);
The compiler creates a tag type in this namespace called tag_name, replaces the expression with a call like that (without even needing to know the declaration of the function):
foo(tag_ns::tag_name{}, 1.f);

If two functions (even in different namespaces) use the same tag name, they will have the same tag type, which is fine.

Yes, initially I was thinking names are names no matter the namespace, but I abandoned this.

I abandoned it because tags are types, and are used for overload resolution. If the std has a tag named 'par' and I want to use this name but with different implementation, I must be able to do that, much like today
We need namespaces for tags, and,  to the very least labels should be defined in the namespace of the functions.

 
The tag has no implementation, only the overload using the tag has an implementation. So there is absolutely no problem here.
void foo(tag:);
void bar(tag:);

Why tag in both cases couldn't be the same? We use nothing from those tag objects.

Tags are used for specialization of functions (parallel library is an example). I must be able to declare tags that are named the same as the std ones, yet are different type and will pick my overload, not the std one. 

I don't think it is necessary to be useful.
 

If we completely remove namespaces for tags people will be forced to go the C-way of naming to be able to create new type under the "same name" - cuda_parallel - to force new overload.

What do you gain with:
foo(cuda::parallel: ...);
over:
foo(cuda::parallel, ...);
?

If it is only to be able to put a colon here, then it is not worth the effort.
Also, I really don't like the three colons.
 
 

How many real world code use non-empty classes as tags? 
 

But then I also abended automatic introduction...
 
...

The main feature here would be the automatic introduction of types. A bit like a template instantiation. It is implicit.

Automatic introduction is not that good of a barging - we invent new rules to save some typing, but new problems arise. 

What are those problems ?
If tag: refers to a single type wherever you are (even across TUs), then what is the problem?
 

Where are tags created? How can we make them template? How can we add attributes to them? Is this really new signature?

First, I was thinking in an unutterable namespace, yet common to all TUs.
But then, making them an instantiation of std::tag<"tag_name"> might be better.

You don't add attributes to them because you don't need to. Do you add attributes to the definition of std::string?
 

Sidenote, how do you name the type to create a function signature? decltype(name:)? name: to mean different things in different contexts? Things escalate quickly. 

 
When you declare a function, you would do:
void foo(tag_name:);
// or
// void foo(std::tag<"tag_name">);

That's true name: would have different meaning depending on the context, but that's would be the case anyway because of labels.

And what about std::function and pointers to functions? We will need to touch a lot of places to enable this syntax.  

If by "a lot", you mean 3, I agree:
- parameter list of a function declarator
- call expression
- decltype expression (not even necessary)

int main() {
 
// Parameter list of a function declarator
 
int foo(name: int); // -> int foo(std::tag<"name">, int);
  std
::function<int(name: int)> f = foo; // -> std::function<int(std::tag<"name">, int)> f = foo;
 
int (*g)(name: int) = static_cast<int(*)(name: int)>(&foo); // -> int (*g)(std::tag<"name">, int) = static_cast<int(*)(std::tag<"name">, int)>(&foo);
 
using foo_t = int(name: int); // -> int(std::tag<"name">, int)

 
// call expression
  foo
(name: 1); // -> foo(std::tag<"name">{}, 1);

 
// decltype expression
 
decltype(name:) h; // -> std::tag<"name"> h;
}

This is the beauty of this: by modifying slightly the language at 3 places, it works nicely. There is no complicated rules.
If name: appears in a parameter list of a function declarator, replace it with std::tag<"name">,
If name: appears in a call expression, replace it with std::tag<"name">{},
if decltype(name:), replace the whole decltype expression with std::tag<"name">

You also need to define the class template std::tag, but that will be trivial.


Also note that we will need extra rules what introduces and what just names a label. And these are not trivial, we can't always blindly introduce. 
Makes no sense to introduce a type by declaring a function pointer, even if we agree to not have namespaces for them.

If you need a tag, you need a tag even when declaring a function pointer.
And yes, we can blindly introduce tag types. How is it different from template instatiation?

 

 
And actually, when declaring a function and when using it, they don't mean the same language construct, but still have a common meaning: they are tags.
So only the translation would be different, not the meaning.

Actually, with decltype(name:), whatever the translation of name: would be (type or object of that type), the result is still the same.

 

In the end, I made it all as conservative as possible and as much based on current practice as possible. The initial simplicity was deceiving, because it was just a morning idea.

On the contrary, the initial simplicity was great. I think you shouldn't have tried to solve its issues by making more complex, but on the contrary making it more simple (like I did).


It is more simple, in the sense, it maps 100% on to established practice and 95+% on current types, declarations, scope, etc, rules 

What you say is not simpler, it is more complete.
When I say simpler, I mean the rules are simpler (a smaller wording).
I never said that my proposition covers as much as yours.
 

 
 

This is not to say we could not use some shortcut syntax.

We could

int foo(tag_name: float f);

generate

case struct tag_name; //< enclosing scope
int foo(case tag_name, float f);

But I still question the pros/cons ratio. 




In your first proposal, you mentioned you wanted to be able to do something like that:
any(in_place<string>: "");
I don't think it is useful. you are not naming an argument here.

In most cases tags are used it is not as if the argument is named. 

 
True, tags are not names.
Avoiding this use case makes the design very simple.
Much simpler than all the alternatives you tried.

Why should I avoid this case, I don't understand? The colon is perfectly deducible in different contexts to mean slightly different things, but it always stands for a clarification.

Here, it is not the colon the problem, but the arbitrary expression on the left of the colon.
Supporting only arbitrary identifier tokens on the left of the colon simplifies very much the wording of the feature: the rules are easy.
 

mihailn...@gmail.com

unread,
Aug 8, 2018, 9:13:57 AM8/8/18
to ISO C++ Standard - Future Proposals, mihailn...@gmail.com, floria...@gmail.com
You are right, still I don't want tags to have any uses that labels can't cover. Otherwise we invent yet another feature, adding up to the language. If we can eliminate an idiom with a feature it will be a win.
 
 
 

If we completely remove namespaces for tags people will be forced to go the C-way of naming to be able to create new type under the "same name" - cuda_parallel - to force new overload.

What do you gain with:
foo(cuda::parallel: ...);
over:
foo(cuda::parallel, ...);
?

If it is only to be able to put a colon here, then it is not worth the effort.
Also, I really don't like the three colons. 


If foo is not in cuda, and if foo is declared to take the tag as template argument (not overloading some other foo with a concrete label) 
Then you gain nothing on the call site, compared to current state of affairs. 

Yet, you don't lose either. If we have to to std::tag<"cuda_parallel"> we go backwards. 
The problem is, there is no rule the language can follow - we need to hardcode all places to parse as either a type or value. On the contrary the case name is just another syntax for namespace resolution and the result is always a type that can be used in any place type is required. 

Also, I don't think being able to see the label is just a type (in a case scope) is a bad thing. 
I don't hold strong opinion as it is learnable either way and as I said am not against that syntax, but just as shortcut. 

having case struct name; introduced, a std::function<int(name: int)> could be a shortcut for std::function<int(case name, int)>

 


You also need to define the class template std::tag, but that will be trivial.


Also note that we will need extra rules what introduces and what just names a label. And these are not trivial, we can't always blindly introduce. 
Makes no sense to introduce a type by declaring a function pointer, even if we agree to not have namespaces for them.

If you need a tag, you need a tag even when declaring a function pointer.
And yes, we can blindly introduce tag types. How is it different from template instatiation?

We are yet to see how tag<"name"> will work in real life, considering it injects a variable. Things are much more simpler if we can name an existing type. 
I see, well as I said, I wanted 100% tags coverage, besides I really like how in_place<string>: reads, but I guess it's subjective.


All in all I am not against what you are saying, for obvious reasons, but I am skeptical if we introduce enough value. 
Tags to be templates is a real-word need, tags to be in a namespaces (sometimes different then the function) is real-word need.

These can't be done by automatic introduction, not without yet even more sugar magic.  

But I might be wrong, these could be left for tags, may be the original idea names-are-names-period is the way to go. It is really, really hard to estimate the usefulness of the feature if it is not "better tags" - will it replace static functions for constructors? Will this be enough? Will there be an upgrade path if someone decides the missing features should be added?


 
 
Reply all
Reply to author
Forward
0 new messages