Closed msadeqhe closed 1 year ago
...TYPE
vs Cpp1-style TYPE(...)
Cpp1 cannot have ...TYPE
syntax, because of literal prefixes and suffixes (compatibility with C).
A(...)
in Cpp1 can be a type, a function, an object or a macro, whereas in this suggestion ...A
or (...)A
in Cpp2 are always types (context-free), and they always call the constructor.TYPE(...)
are mandatory, whereas they are optional in (...)TYPE
for a literal. So ...TYPE
is both UDL and constructor....TYPE
and (: TYPE = ...)
They are the same. ...TYPE
is a syntactic sugar to (: TYPE = ...)
, in a similar manner that OBJ.FUNC(...)
is a syntactic sugar to FUNC(OBJ, ...)
in UFCS. ...TYPE
increases code readability in addition to comfortability of writing code.
...TYPE
is syntactic sugar to (: TYPE = ...)
.
OBJ.FUNC(...)
is syntactic sugar to FUNC(OBJ, ...)
.
...TYPE
and control structuresInitializing is supported for all control structures in Cpp2:
(copy i: = 0) while i < 10 next i++ {
/*{- statements... -}*/
}
The parentheses before while
is like declaring parameters for it. But the parentheses before TYPE
is like passing arguments to it. So the parentheses before a KEYWORD
(e.g. while
, if
, for
, ...) are for parameterized block statements, and the parentheses before a TYPE
are for passing arguments to the constructor:
// -- Pass arguments to `TYPE`s constructor.
(1, 2)TYPE
// -- Declare parameters for `while`, `if`, `for`, ...
(copy i: = 0) KEYWORD...
...TYPE
and postfix operatorsOnly a literal (without postfix operators) or parentheses may be immediately before TYPE
:
a0: = 10ull++; // -- OK.
b0: = 10++ull; // -- ERROR!
a1: = (10ull)++; // -- OK.
b1: = (10++)ull; // -- OK.
...TYPE
and prefix operatorsConstructor calls have higher precedence than prefix operators:
x: = -10ull; // -- It's equal to -(10ull)
y: = !"text"something; // -- It's equal to !("text"something)
...TYPE
and immediately call operator()
and operator[]
operator()
and operator[]
are postfix operators, they are called after object construction:
x: = (1, 2)TYPE(); // -- It's equal to ((1, 2)TYPE)();
y: = (1, 2)TYPE[0]; // -- It's equal to ((1, 2)TYPE)[0];
...TYPE
after operator()
and operator[]
and variable templatesThese are corner cases. They can be banned, although they are syntactically correct (left to right):
x: = object()TYPE; // -- It's equal to (object())TYPE
y: = object[0]TYPE; // -- It's equal to (object[0])TYPE
z: = pi<ulong>TYPE; // -- It's equal to (pi<ulong>)TYPE
I think the decision is related to Cpp2's goals. By the way, it's safe not to support these corner cases.
Been trying to give some feedback for some days but idk what to say. This suggestion builds up on Herb's {constructor × assignment} unification by making suffixes as contructor which, when you think about it makes a lot of sense. I have one question, how does this play out with something like std::string's literal?
using namespace std::literals;
//Cpp example
auto str1 = "hi y'all"s;
auto str2 = std::string{"hi again"};
//both work
using namespace std::literals;
//cpp2 example
str1 := "hi a third time"std::string;
str2 := "last hi"s; //would this work?
Other than that, I think this suggestion is great (I would like anything that prevents me from writing the type between : and =)
But I would also like to see how issue #451 is solved, maybe Herb could come up with something combined with this that also keeps the operator= as a binary operator.
Thanks for your feedback. Yes, that Cpp2 example would work. Herb stated in this comment, he want to support consuming UDLs, but he didn't decide on whether or not to support authoring UDLs yet.
It's possible to have UDLs with the same name of types. In this case, types will be prefered over UDLs. For example:
// abc: type;
// UDL in Cpp1
abc operator ""abc(const char *str, std::size_t len) {
return (: std::string = (str, len));
}
// Type declaration in Cpp2
abc: type = {
operator=: (out this, value: std::string) = {}
}
main: () = {
// It won't call UDL function.
// It would call the type's constructor.
object: = "text"abc;
}
On the other hand, can UDLs be used in place of types? Two options may be considered:
// UDL in Cpp1
unsigned long long operator ""ull(unsigned long long value) {
return value;
}
main: () = {
// It would call UDL function: operator ""ull(1'000)
object: ull = 1'000;
}
2. UDLs cannot be used in place of types.
Option 1 is generalized for object construction, similar to how UFCS works on functions. Option 1 would make UDLs to behave like they are non-member constructors, IMO it's better than option 2.
### Declaration syntax in Cpp2;
### UDLs are Non-member Constructors.
But if the plan is to support authoring them, if we look at how semantically they are related to types' constructors, the following syntax seems reasonable for them, especially if the plan is to allow UDLs to be used in place of types:
```cpp
// in Cpp1:
// RETURNTYPE operator ""SUFFIX(ARGTYPE ARG) {...}
SUFFIX: (ARG: ARGTYPE) -> type == RETURNTYPE = {
// -- statements...
}
That means, they are functions in which their return type is a type alias. For example:
// in Cpp1:
// unsigned long long operator ""ull(unsigned long long value) {...}
ull: (value: ulonglong) -> type == ulonglong = {
return value;
}
main: () = {
x: = 1'000ull;
}
In this case, UDLs in Cpp1 are changed to mean Non-member Constructors in Cpp2.
Shouldn't your last bit of cpp2 code
main: () = { x: = 1'000ull; }
Have a function call, rather than a UDL?
main: () = { x: = 1'000.ull(); }
On 18 May 2023 08:29:49 Sadeq @.***> wrote:
Thanks for your feedback. Yes, that Cpp2 example would work. Herb stated in [this comment][1], he want to support consuming UDLs, but he didn't decide on whether or not to support authoring UDLs yet.
UDLs
It's possible to have UDLs with the same name of types. In this case, types will be prefered over UDLs. For example:
// abc: type;
// UDL in Cpp1 abc operator ""abc(const char *str, std::size_t len) { return (: std::string = (str, len)); }
// Type declaration in Cpp2 abc: type = { operator=: (out this, value: std::string) = {} }
main: () = { // It won't call UDL function. // It would call the type's constructor. object: = "text"abc; }
On the other hand, can UDLs be used as types? Two approaches may be considered:
// UDL in Cpp1 unsigned long long operator ""ull(unsigned long long value) { return value; }
main: () = { // It would call UDL function. object: ull = 1'000; }
// Type alias in Cpp2 str: == std::string;
object = "text"str;
UDLs are Non-member Constructors (declaration syntax in Cpp2)
But if the plan is to support authoring them, if we look at how semantically they are related to types' constructors, the following syntax seems reasonable for them, especially if the plan is to allow UDLs to be used in place of types:
// in Cpp1: // RETURNTYPE operator ""SUFFIX(ARGTYPE ARG) {...} SUFFIX: (ARG: ARGTYPE) -> type == RETURNTYPE = { // -- statements... }
That means, they are functions in which their return type is a type alias. For example:
// in Cpp1: // unsigned long long operator ""ull(unsigned long long value) {...} ull: (value: ulonglong) -> type == ulonglong = { return value; }
main: () = { x: = 1'000ull; }
In this case, UDLs in Cpp1 are changed to mean Non-member Constructors in Cpp2.
— Reply to this email directly, view it on GitHubhttps://github.com/hsutter/cppfront/issues/455#issuecomment-1552639875, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AALUZQP4VHME7YBSDE3OXO3XGXFWVANCNFSM6AAAAAAYAQFN4M. You are receiving this because you are subscribed to this thread.Message ID: @.***>
Shouldn't your last bit of cpp2 code
main: () = { x: = 1'000ull; }
Have a function call, rather than a UDL?
main: () = { x: = 1'000.ull(); }
It doesn't need parenthesis, because the idea is to make ...TYPE
or ...SUFFIX
to call TYPE
's constructor or SUFFIX
UDL function respectively.
I have to explain that if the constructor requires multiple arguments, it would be called like (arg1, arg2, ...)TYPE
.
On the other hand, UDL functions cannot have multiple parameters, therefore it won't be called like (arg1, arg2, ...)SUFFIX
, because SUFFIX
would only work on a single argument, so (10)suffix
and 10suffix
are correct.
On the other hand, UDL functions cannot have multiple parameters, therefore it won't be
(arg1, arg2, ...)SUFFIX
, becauseSUFFIX
would only work on a single literal without parenthesis, so(10)suffix
is wrong and10suffix
is correct.
I've corrected that to this:
On the other hand, UDL functions cannot have multiple parameters, therefore it won't be
(arg1, arg2, ...)SUFFIX
, becauseSUFFIX
would only work on a single argument, so(10)suffix
and10suffix
are correct.
Because optional parenthesis and allowing UDLs to work on expressions, will help without any conflict or ambiguity.
Ah, apologies, I believe thanks to UFCS the
.ull()
Already works in cpp2, so why add another way of doing the same thing?
On 18 May 2023 08:58:34 Sadeq @.***> wrote:
Shouldn't your last bit of cpp2 code
main: () = { x: = 1'000ull; }
Have a function call, rather than a UDL?
main: () = { x: = 1'000.ull(); }
It doesn't need parenthesis, because the idea is to make ...TYPE or ...SUFFIX to call TYPE's constructor or SUFFIX UDL function respectively.
I have to explain that if the constructor requires multiple arguments, it would be called like (arg1, arg2, ...)TYPE.
On the other hand, UDL functions cannot have multiple parameters, therefore it won't be (arg1, arg2, ...)SUFFIX, because SUFFIX would only work on a single literal without parenthesis, so (10)suffix is wrong and 10suffix is correct.
— Reply to this email directly, view it on GitHubhttps://github.com/hsutter/cppfront/issues/455#issuecomment-1552685706, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AALUZQOPCEBFZKSAUMA2MTDXGXJCFANCNFSM6AAAAAAYAQFN4M. You are receiving this because you commented.Message ID: @.***>
I have to explain that if the constructor requires multiple arguments, it would be called like
(arg1, arg2, ...)TYPE
.On the other hand, UDL functions cannot have multiple parameters, therefore it won't be called like
(arg1, arg2, ...)SUFFIX
, becauseSUFFIX
would only work on a single argument, so(10)suffix
and10suffix
are correct.
If Herb accepts to support authoring UDLs like they are non-member constructors, in this case also SUFFIX
may have multiple parameters, and it can be called like (arg1, arg2, ...)SUFFIX
.
Ah, apologies, I believe thanks to UFCS the .ull() Already works in cpp2, so why add another way of doing the same thing?
Because:
It's surprising to me, because:
- UFCS is about Unifying Function Call Syntax, and suddenly it works with types.
- It doesn't feel expressive enough for a context-free language.
: Type = (args)
creates a variable, butA(args)
(alsoa.A(args)
) may create a variable or may call a function (or function object).- It's like accessing a base class within multiple inheritance, e.g.
a.Base::call()
in Cpp1.Also it would conflict with multiple inheritance in Cpp2, it depends on how we would access base types:
Base1: type = { operator=: (out this) = {} operator=: (out this, v: x) = {} operator(): (this) -> int = 0; } Base2: type = { operator=: (out this) = {} operator=: (out this, v: x) = {} operator(): (this) -> int = 0; } x: type = { this: Base1 = (); this: Base2 = (); variable: Base1 = (); operator(): (this) -> int = { // It calls operator(). m: = this.variable(); // Does it call operator() from Base1? // or calls the constructor with `Base1(this)`? // It's ambiguous because of UFCS on types. n: = this.Base1(); return 0; } }
In example above,
this::Base1()
can be another syntax option, but that resembles scope resolution operator (e.g.namespace::...
ortype::...
) which doesn't look uniform to how we access members ofthis
.It would complicate the language, similar to how
Type(...)
has complicated Cpp1 for object construction and function declaration in Most Vexing Parse. It's better to distinguish types from functions and variables syntactically in addition to semantically.I couldn't find main reasons that why it's surprising to me. Now I've found them:
UFCS is syntactically and semantically incorrect for types.
UFCS is about to unify
function(a, args)
(non-member functions) witha.function(args)
(member functions).
- It's important to note that both of them are valid syntax for functions without UFCS.
On the other hand,
Type(a, args)
is unified witha.Type(args)
.
- But the problem with
a.Type(args)
is that itself is not a valid syntax without UFCS!- It must be
A::Type(args)
to be a valid nested type, because nested types always need scope resolution operator.So UFCS on types would unify
Type(a)
(object construction) with an invalid syntaxa.Type()
(nested type which has to beA::Type()
). That's the reason why I think UFCS on types are incorrect.
- It's inconsistent with nested types, thus what's the point of UFCS on types? For example:
A: type = { X: type = {} } B: type = { operator=: (out this, a: A) = {} } main: () = { a: A = (); // It works. // It's equal to `B(a)`. m: = a.B(); // a.B() == B(a) // ERROR! It doesn't work. // It must be `A::X()`. n: = a.X(); // a.X() != A::X(a) }
So
a.Type(arg)
would lead to surprises on types, because it doesn't work on nested types.UFCS on types is in contrast to the purpose of
operator.
which is to access members!
- UFCS on types would make member functions to conflict with
a.SOMETHING(args)
.Member functions and types are completely different, but unwillingly they will impact each other. It's is in contrast with UFCS for functions in which it only impacts on what function to call.
abc: type = { klass: (this) = {} } klass: type = { operator=: (out this, v: abc) = {} } main: () = { a: abc = (); // It conflicts... // Does it call the constructor of `klass`? // or it calls the member function `klass`? a.klass(); }
In this example, the meaning of
a.klass()
would be ambiguous.
- For any type named
klass
, semanticallya.klass()
is inconsistent with member access.In contrast,
o.func()
andfunc(o)
for functions are semantically consistent with member access, the first argument is the object.But
a.klass()
andklass(a)
for types are semantically inconsistent with member access, the first argument is not the object, it's just an argument which shouldn't be used like an object. Types don't have enough relation to UFCS.a.Type() --> operator=(out this, a) a.func() --> func(a) // `this = a` for member functions
Those reasons are from this issue.
Yes, user defined literals are a type of function call, just one with a weird syntax (what does operator"" have to do with numerical literals anyway?!) and unique rules that need to be taught, UFCS seems ideal for this IMO.
literal.function() is the same as function(literal), as per UFCS, the same as everywhere, I don't see any room for ambiguity, unless there is a function and a type with the same name and signature, but then I'm not sure if that is valid anyway?
Your inheritance example, why do your base classes both have constructors requiring an instance of the child type? Is this valid code? Also this example doesn't contain literals so I'm not sure how it is relevant?
Where you say
a.A(args)) may create a variable or may call a function (or function object).
A is not a literal in this example, and A(args) doesn't seem to be a UDL either, but for completeness
1.MyIntegerType() 1.funcReturningMyIntegerType()
Being replaced by
MyIntegerType(1) funcReturningMyIntegerType(1)
Seems fine, and in both cases this is really very equivalent. In both cases an instance is created, and in both cases a function call occurs, it just so happens that one of those calls is a constructor function.
Are you perhaps trying to report a bug with UFCS with relation to classes and multiple inheritance?
Or perhaps I need to log into GitHub and stop doing all this via email... Apologies if I'm missing some greater context here
On 18 May 2023 09:16:46 Sadeq @.***> wrote:
Ah, apologies, I believe thanks to UFCS the .ull() Already works in cpp2, so why add another way of doing the same thing?
Because:
It's surprising to me, because:
Also it would conflict with multiple inheritance in Cpp2, it depends on how we would access base types:
Base1: type = { operator=: (out this) = {} operator=: (out this, v: x) = {} operator(): (this) -> int = 0; }
Base2: type = { operator=: (out this) = {} operator=: (out this, v: x) = {} operator(): (this) -> int = 0; }
x: type = { this: Base1 = (); this: Base2 = (); variable: Base1 = ();
operator(): (this) -> int = {
// It calls operator().
m: = this.variable();
// Does it call operator() from Base1?
// or calls the constructor with `Base1(this)`?
// It's ambiguous because of UFCS on types.
n: = this.Base1();
return 0;
}
}
In example above, this::Base1() can be another syntax option, but that resembles scope resolution operator (e.g. namespace::... or type::...) which doesn't look uniform to how we access members of this.
It would complicate the language, similar to how Type(...) has complicated Cpp1 for object construction and function declaration in Most Vexing Parse. It's better to distinguish types from functions and variables syntactically in addition to semantically.
I couldn't find main reasons that why it's surprising to me. Now I've found them:
UFCS is syntactically and semantically incorrect for types.
UFCS is about to unify function(a, args) (non-member functions) with a.function(args) (member functions).
On the other hand, Type(a, args) is unified with a.Type(args).
So UFCS on types would unify Type(a) (object construction) with an invalid syntax a.Type() (nested type which has to be A::Type()). That's the reason why I think UFCS on types are incorrect.
A: type = { X: type = {} }
B: type = { operator=: (out this, a: A) = {} }
main: () = { a: A = ();
// It works.
// It's equal to `B(a)`.
m: = a.B();
// a.B() == B(a)
// ERROR! It doesn't work.
// It must be `A::X()`.
n: = a.X();
// a.X() != A::X(a)
}
So a.Type(arg) would lead to surprises on types, because it doesn't work on nested types.
UFCS on types is in contrast to the purpose of operator. which is to access members!
Member functions and types are completely different, but unwillingly they will impact each other. It's is in contrast with UFCS for functions in which it only impacts on what function to call.
abc: type = { klass: (this) = {} }
klass: type = { operator=: (out this, v: abc) = {} }
main: () = { a: abc = ();
// It conflicts...
// Does it call the constructor of `klass`?
// or it calls the member function `klass`?
a.klass();
}
In this example, the meaning of a.klass() would be ambiguous.
In contrast, o.func() and func(o) for functions are semantically consistent with member access, the first argument is the object.
But a.klass() and klass(a) for types are semantically inconsistent with member access, the first argument is not the object, it's just an argument which shouldn't be used like an object. Types don't have enough relation to UFCS.
a.Type() --> operator=(out this, a)
a.func() --> func(a) // this = a
for member functions
Those reasons are from this issuehttps://github.com/hsutter/cppfront/issues/284.
— Reply to this email directly, view it on GitHubhttps://github.com/hsutter/cppfront/issues/455#issuecomment-1552706931, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AALUZQJ6CCFFRHFSLJPK4S3XGXLGNANCNFSM6AAAAAAYAQFN4M. You are receiving this because you commented.Message ID: @.***>
Yes, user defined literals are a type of function call, just one with a weird syntax (what does operator"" have to do with numerical literals anyway?!) and unique rules that need to be taught, UFCS seems ideal for this IMO.
I'm agree with you except the part about UFCS. Instead of using UFCS to replace UDLs, let's fix that problems. Cpp2 can have a different syntax for declaring UDLs. The following is just an example (its syntax can be anything else):
suffix: (value: ulong) -> type == SomeType = {
// statements...
}
I should mention I don't suggest to support authoring UDLs in Cpp2 (it's just a possibility to consider).
I suggest to change the syntax of object construction from TYPE(...)
to (...)TYPE
, therefore UDLs would be completely replaced with constructors.
literal.function() is the same as function(literal), as per UFCS, the same as everywhere, I don't see any room for ambiguity, ...
Yes, that's the problem. It works, but it doesn't worth it. In a nutshell, the problems with a.Type(args)
are that:
a
doesn't have member Type
.::
) for referring to types.a
is not the first argument of Type
's constructor in operator=: (out this, args)
.
a
is the first argument (this
argument).a
is not the first argument (this
argument) of constructor function.Type
declaration to see if that's a type or a callable.a
is the object to work with it.a
is not the object to work with it, a
and args
are arguments to construct a new object.a
has always exactly the same behaviour as args
.
Now, let's consider these examples of how a.Type(args)
may go wrong:
Connection: type = {
operator=: (out this, timeout: uint) = {}
operator=: (out this, timeout: uint, proxy: my::proxy) = {}
operator=: (out this, encrypted: bool, timeout: uint, proxy: my::proxy) = {}
}
main: () = {
x: = 2000.Connection();
// Are they related to UFCS and UDLs? No.
y: = 2000.Connection(my::proxy());
z: = true.Connection(2000, my::proxy());
}
IMO that code is unreadable.
The problem with a.Type(arg)
(UFCS on types) is that a
is not the object (this
argument), it's just like other args
in which the interface is not prepared for it, it leads to unreadable code. But in a.func(args)
(UFCS on functions), a
is the object (this
argument), it leads to readable code.
Thanks, this is really clear now I understand.
UDL has historically required a function definition, which still works in cpp2 via UFCS, however you want to succinctly specify what the type is of the literal, without the need to create a function, and then call it.
In cpp1 you can use size_t{1} with the added benefit that the literal is bounds checked.
In cpp2 the following is difficult
foo : uint64 = ~0; bar : uint64 = 0xffffffffffffffff;
Are these equivalent values? Or is ~0 an int32?
UFCS way (requires an extra function definition) ull := (in x : uint64) { return x; } foo : uint64 = ~0.ull();
Your way (presumably you're expecting the constructor to take precedence over the ~operstor) foo := ~0uint64
Built in types don't have a constructor so does this work in cppfront? foo := ~uint64(0)
I see where you're coming from, however it does add to the concept count of the language.
How does your proposition handle the difference between built in and user defined types?
On 18 May 2023 12:20:53 Sadeq @.***> wrote:
Yes, user defined literals are a type of function call, just one with a weird syntax (what does operator"" have to do with numerical literals anyway?!) and unique rules that need to be taught, UFCS seems ideal for this IMO.
I'm agree with you except the part about UFCS. Instead of using UFCS to replace UDLs, let's fix that problems. Cpp2 can have a different syntax for declaring UDLs. The following is just an example (its syntax can be anything else):
suffix: (value: ulong) -> type == SomeType = { // statements... }
I should mention I don't suggest to support authoring UDLs in Cpp2 (it's just a possibility to consider). I suggest to change the syntax of object construction from TYPE(...) to (...)TYPE, therefore UDLs would be completely replaced with types.
literal.function() is the same as function(literal), as per UFCS, the same as everywhere, I don't see any room for ambiguity, ...
Yes, that's the problem. It works, but it doesn't worth it. In a nutshell, the problems with a.Type(args) are that:
— Reply to this email directly, view it on GitHubhttps://github.com/hsutter/cppfront/issues/455#issuecomment-1552911601, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AALUZQJ7FY777GXAW7A4JQDXGYAY3ANCNFSM6AAAAAAYAQFN4M. You are receiving this because you commented.Message ID: @.***>
I agree, but again, there is no implicit this in cppfront, so I don't think literal calls via UFCS can call type members anyway
On 18 May 2023 12:57:47 Sadeq @.***> wrote:
Now, let's consider these examples of how a.Type(args) may go wrong:
Connection: type = { operator=: (out this, timeout: uint) = {} operator=: (out this, timeout: uint, proxy: my::proxy) = {} operator=: (out this, encrypted: bool, timeout: uint, proxy: my::proxy) = {} }
main: () = {
// No! It's not 2000 connections! It's Connection(2000)
.
x: = 2000.Connection();
// Are they related to UFCS and UDLs? No.
y: = 2000.Connection(my::proxy());
z: = true.Connection(2000, my::proxy());
}
IMO that code is unreadable.
— Reply to this email directly, view it on GitHubhttps://github.com/hsutter/cppfront/issues/455#issuecomment-1552948073, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AALUZQMOV7NCMN5SGGJBL6TXGYFDTANCNFSM6AAAAAAYAQFN4M. You are receiving this because you commented.Message ID: @.***>
Built in types don't have a constructor so does this work in cppfront? foo := ~uint64(0)
Yes, that works with foo: = 0uint64~
(bitwise ~
is postfix in Cpp2). Built-in types don't need constructors because ...TYPE
is a syntactic sugar to (:TYPE=value)
.
I see where you're coming from, however it does add to the concept count of the language.
It changes the syntax of one concept from TYPE(args)
to (args)TYPE
. Also it would completely eliminates the concept of built-in literal prefixes and suffixes and UDLs. Although I hope Cpp2 to support authoring UDLs as they are likely non-member constructors.
How does your proposition handle the difference between built in and user defined types?
They wouldn't be a separate concept. Infact there wouldn't be any built-in literal prefixes and suffixes. All of them would be UDLs for built-in types (if needed). They can be used with types and type aliases together in a similar way:
ull: type == ulonglong;
something: type = {
operator=: (out this, value: ull) = {}
}
// -- UDL (aka Non-member Constructor) declaration example in Cpp2
suffix: (value: ull) -> type == something = (value)something;
main: () = {
// -- All of them are valid.
a: = (10)int;
b: = 10int;
c: = 10ull;
d: = 10something;
e: = (10ull)something;
f: = (10ull)suffix;
g: = (10)suffix;
h: = 10suffix;
}
I agree, but again, there is no implicit this in cppfront, so I don't think literal calls via UFCS can call type members anyway
If I understand your response correctly, they would be called as reported in this comment by @JohelEGP, because when Cpp2 transforms a.Type(args)
to Type(a, args)
, if literal (or variable) a
is int
and the parameter type is int
, implicit conversion won't be happened, because that's a direct call.
Semantically it's meaningless, because
a
has always exactly the same behaviour asargs
.
- That's a useless visual separation.
I have to clarify that with (...)TYPE
, that useless visual separation doesn't exist for multiple arguments. For example:
// This separation between `true` and other arguments are meaningless.
// Using TYPE(...) and UFCS on types for object construction:
x0: = true.Connection(2000, my::proxy());
// All arguments have the same behaviour on object construction.
// Using (...)TYPE for object construction:
x1: = (true, 2000, ()my::proxy)Connection;
In x0
That useless visual separation is misleading for object construction.
But in x1
all arguments are truly visually with together.
That's not a problem exclusive to types. Not all functions have a nice flow using UFCS.
x0 := true.connect(2000, my::proxy());
That's not a problem exclusive to types. Not all functions have a nice flow using UFCS.
x0 := true.connect(2000, my::proxy());
That's a problem exclusive to types, because the first argument of member functions is already an object this
of a type. In your example, that doesn't work for explicit this
, because this
parameter of member functions cannot be of type bool
.
You're mixing up two things. My comment was about the general flow of UFCS.
As for the out this
parameter of an operator=
,
the result object is implied by the call-site syntax to create an object.
So there's no argument for the explicit this
parameter.
Maybe I didn't understand your comment correctly, but that's what I'm trying to explain that a.Type(args)
is a bad mix of UFCS and object construction.
I don't have a problem with how a.Type(args)
works, my problem is that why it works!
Consider I want to explain about UFCS on types for novice programmers in the following paragraphs.
this
object:function: (value: int, arg: int) = {}
x: = 10.function(10);
this
object:Something: type = {
function: (this, arg: int) = {}
}
a: Something = ();
x: = a.function(10);
out this
and we can't call it with this
object. That's right but why the second parameter of constructors can be used as this
object in UFCS? While the first parameter is expressively written syntactically to be this
object!Something: type = {
operator=: (out this, arg1: int, arg2: int) = {}
}
x: = 10.Something(10);
OK. This example is one of the reasons I think UFCS on types are not natural. You get visually similar syntax for both UFCS on functions a.func(args)
and on types a.Type(args)
, but it leads to inconsistent syntax and semantic for object constructions, nested types and etc which I explained before.
That could be convincing. Let's try to look at the type's name as the implicit first argument to stimulate the mind.
x: Type = (a, 0); // Arguments: (`Type`, `a`, `0`).
x := :Type = (a, 0); // Arguments: (`Type`, `a`, `0`).
x := Type(a, 0); // Arguments: (`Type`, `a`, `0`).
x := Type(a, 0); // Callable: `Type`, arguments: (`a`, `0`).
x := a.Type(0); // Arguments: (`Type`, `a`, `0`), out of order.
x := a.Type(0); // Callable: `Type`, arguments: (`a`, `0`).
x := a.func(0); // Callable: `func`, arguments: (`a`, `0`).
I make no conclusions so far.
I think authoring UDLs shouldn't be a thing if this suggestion is implemented, could make thing confusing IMO. Other than that, I don't see a problem with this suggestion. Though it is kind of surprising when seen at first but makes sense when you see that it is generalised from suffixes OR the other way around, suffices could be generalised from this.
@JohelEGP Good point. I change them to Cpp2 function signatures:
// `obj` is not `this`. It can't be the object.
x: Type = (obj, 0); //--> (out this, obj, 0)
x: =: Type = (obj, 0); //--> (out this, obj, 0)
x: = Type(obj, 0); //--> (out this, obj, 0)
// `obj` can be the object.
x: = func(obj, 0); //--> func(inout this = obj, 0)
// `obj` is not `this`. It can't be the object.
// Why is `obj` treated like the object?
// Whereas `out this` is the object.
x: = obj.Type(0); //--> (out this, obj, 0)
// `obj` can be the object.
x: = obj.func(0); //--> func(inout this = obj, 0)
So both inout this
and in this
are consistent with how UFCS works, but out this
has different (inconsistent) behaviour in UFCS. Briefly, I think UFCS shouldn't work on operator=
(constructors).
Type(obj, ...)
is a function notation for Type
to call its constructor, because of UFCS it must be equal to obj.Type(...)
notation. But obj.Type(...)
itself is not valid (inconsistent with operator dot, nested types and this
parameter).
@AbhinavK00 You're right. The syntax is like literal suffixes. They are expressive for object construction.
I'm agree that (...)TYPE
is not similar to function calls, that's intentional, so UFCS won't work on them.
Authoring UDLs is only a possibility to consider. As you said, it would increase concept count. By the way, UDLs can be Non-member Constructors in my suggestion.
I have to mention, (arg)Type
for object construction is left to right as the same as a: Type
for declaration is left to right. In this way, the type would always come after the identifier of the declaration, the literal, or the arguments of the constructor. But Type(arg)
(Cpp1-style) doesn't follow this rule.
...TYPE
afteroperator()
andoperator[]
and variable templatesThese are corner cases. They can be banned, although they are syntactically correct (left to right):
x: = object()TYPE; // -- It's equal to (object())TYPE y: = object[0]TYPE; // -- It's equal to (object[0])TYPE z: = pi<ulong>TYPE; // -- It's equal to (pi<ulong>)TYPE
I think the decision is related to Cpp2's goals. By the way, it's safe not to support these corner cases.
I'm thinking about this use case... what if Cpp2 supports it too?
Consider we already have Function Chaining in C++:
x: = fetch("something").filter(10).sort(true);
With (...)Type
chaining, we would have:
x: = fetch("something")list.filter(10)list.sort(true);
It would allow us to specify the types within Function Chaining. For example, it would be possible to have mytype
and vector
instead of list
in that example:
x: = fetch("something")mytype.add(10)vector.size();
IMO that's useful. Optionally it's a possibility to consider for Cpp2.
Also type composition (derived units) are possible with <>
, but parentheses are mandatory except for literals:
A: type = { /*declarations*/ }
B: type = { /*declarations*/ }
two: = 2;
i: = 2A;
j: = 2<A*B>;
k: = (two)<A*B>;
m: = (1, 2)A;
n: = (1, 2)<A*B>;
x: A = (1, 2);
y: <A*B> = (1, 2);
// <T> is template parameter.
r: <T> A = (1, 2);
s: <T> <A*B> = (1, 2);
I have to correct my suggestion about derived units e.g. <A*B>
in previous comment:
<A * B>
is always A
.
A op B
is always A
.<A && B>
is always bool
.
A op B
is always bool
.<A += B>
is always A&
.
A op B
is always A&
.<A*>
aka Indirection<A&>
aka Address-of<A()>
aka Function Call<A[]>
aka SubscriptFor arithmetic, logical and assignment operators, the type of expression is always known from themselves. So <>
is not needed at all to get the type of them. Also <A*>
, <A&>
, <A()>
and <A[]>
operations are not useful within derived units.
TLDR; (args)Type
is considered better than other alternatives.
(args):Type
The advantage of (args)Type
over (args):Type
is that it won't change the meaning of declaration syntax within expressions:
// `a: Type` is a declaration.
a: Type = 2;
// But `a:Type` is a typed expression here.
b: = a:Type;
// `(a)Type` always is an expression and creates an object.
c: = (a)Type;
That's important because maybe Cpp2 will support named arguments or designated initialization with the following syntax:
// named arguments
x: = call(name: string = "someone", age: int = 20);
// designated initialization
m: Type = (name: string = "someone", age: int = 20);
Type(args)
The advantage of (args)Type
over Type(args)
(Cpp1-style) is that:
(arg).Type(other_args)
The advantage of (args)Type
over (arg).Type(other_args)
(Cpp1-style UFCS) is that:
All arguments are at the same side, because they have the same characteristic in object creation:
// This is misleading, although 1 and 2 have the same charactersistic.
u: = 1.point(2);
// OK. `v` is a `point` with value `(1, 2)`.
v: = (1, 2)point;
(args).Type
The advantage of (args)Type
over (args).Type
is that it's context-free.
(: Type = (args))
They complement each other. The advantage of (args)Type
over (: Type = (args))
is that:
It doesn't need extra parenthesis around itself:
// Extra parenthesis is needed here.
u: = (: point = (1, 2)) * (: point = (1, 2));
// no extra parenthesis
v: = (1, 2)point * (1, 2)point;
They are completely different, but parenthesis before a type is syntactically similar to parenthesis before a control structure:
// Parenthesis before a control structure, will initialize variables within them.
(copy i: int = 0) while i <= 10 next i++ {
std::print(i);
}
// Parenthesis before a type, will initialize an object.
x: = (1, 2)point;
Consider the syntax of object creation is (args)class
and class
is a type. These are use cases.
As described in the suggestion, every object construction with (args)class
is like a literal suffix:
a: = (2)int;
b: = ("text")string;
Optionally the parentheses may be omitted for literals (e.g. 2 int
and "text" string
). On the other hand, you may force to always write parentheses as they would resemble function calls.
So it would eliminate the need of built-in literal suffixes and prefixes.
It would be possible to construct objects within operator chaining (member access operators, unary postfix operators, operator()
, operator[]
and etc):
level: type = { /*...*/ }
something: type = { /*...*/ }
x: = (2++)level++.member(true)something()++;
Also (arg).class(other_args)
(UFCS on types) has this advantage too, but unfortunately it has some problems which is described before.
If (args_1)class(args_2)
would immediately call operator()
(with argument args_2
) after object creation with (args_1)class
, it would resemble operator class
within operands (args_1)
and (args_2)
.
add: type = {
data: int;
operator=: (out this, arg: int) = {
data = arg;
}
operator(): (this, value: int) -> int = data + value;
}
a: = 2;
b: = 2;
// x == 4
x: = (a) add (b);
It looks like we have defined operator add
.
If Cpp2 would support object construction chaining with operator()
, and if we could use Meta Functions to have user-defined control structures, it would be like this:
list: vector<int> = /*...*/;
(copy i: int = 0) @forr (i < 10) nextt (i++) within (list) call (item) { /*...*/ }
So literally nextt
, within
and call
are types but they look like keywords within control structure @forr
which is a Meta Function applied to a parameterized block statement. The use of Meta Function will allow us to calculate arguments (expressions) multiple times (such as i < 10
), because their behaviour would be like macros in Cpp1 as they generate code.
EDIT: I will create a new issue (suggestion) for it when reflections are ready for C++.
EDIT: The issue is created here.
(args)class
seems to be a general language feature to replace other minor features.
Also (args).class
and (args):class
can be considered as alternative notations.
EDIT: I've to clarify (args).class
is not context-free, and (args):class
will change the meaning of variable declaration within expressions, therefore it perhaps will conflict with named function parameters which have explicit types (it depends on the syntax if Cpp2 will support it in the future):
call(name: = "Someone", age: int /*explicit type*/ = 30);
1. Preface
Literal suffixes are syntactic sugars to constructors. Considering Unified Function Call Syntax for Member Functions and Non-member Functions, this suggestion is somehow something similar for Literal Suffixes and Constructors.
Briefly, I suggest to support this:
I have to explain:
ul
ands8
are type aliases in1'000ul
and"text"s8.size()
respectively.(2.5litre)water
creates a variable fromlitre
s constructor, and passes it towater
s constructor.(name, age)my::player
creates a variable from typeplayer
with(name, age)
constructor.()m4gun
creates a variable from typem4gun
with default constructor.2. Suggestion Detail
Currently, Cpp2 doesn't have a special syntax to directly call constructors. I suggest to directly call Constructors of a type in the form of Literal Suffixes. Let's name it Direct Object Construction Syntax or Constructor Call Syntax.
So
...TYPE
will be a syntactic sugar to(: TYPE = ...)
in Cpp2. For example:It requires to remove all built-in literal prefixes and suffixes:
l
,ll
,ul
,ull
suffixes from integer literals.f
,l
,f8
,f16
,f32
,f64
suffixes from floating-point literals.u
,U
,u8
,u16
,u32
prefixes from both character and string literals.R
and$
prefixes from string literals as described in this issue, will make all literals to be consistent.Constructors and UDLs (user-defined language literals) are two ways in Cpp1 to create objects from literals:
The following expression in Cpp2 satisfies the purpose of both two lines above:
These are some notes to consider:
a: = 1'000ul; // -- It's equal to 1'000ul in Cpp1. b: = "text"s8; // -- It's equal to u8"text" in Cpp1.
()
. For example:name: = ask_player_name(); age: = ask_player_age(); p: = (name, age)my::player; p.buy(()m4gun, 30bullet);
Consider how
...TYPE
is expressive and more readable than(: TYPE = ...)
, that's the reason why Cpp1 has UDLs. For example:It's possible to consume Cpp1 UDLs. For example:
Constructors can replace Cpp1 UDLs completely, but Cpp2 can still support to author UDLs (user-defined literal suffixes, e.g.
operator""suffix
). Probably the plan is to only consume UDLs as described in this comment from @hsutter.3. Your Questions
Will your feature suggestion eliminate X% of security vulnerabilities of a given kind in current C++ code?
No.
Will your feature suggestion automate or eliminate X% of current C++ guidance literature?
Yes, because this change makes Cpp2 to reduce the concept count with a general language feature. So it will be simpler to learn and understand which leads to smaller guidance literature.
...TYPE
, parentheses are not necessary when...
is only one literal.()TYPE
, it calls the default constructor(args...)TYPE
FUNCTION()
, it calls a function without argumentsFUNCTION(args...)
obj.FUNCTION()
obj.FUNCTION(args...)
using
statement before they can be applied to literals._
, thus they will be distinguished from UDLs which are declared in the Cpp1 standard library.4. More Examples
By declaring type aliases to have familiar names:
The process of object constructions will be simpler and readable:
5. Considered Alternatives
This suggestion is a simpler and generalized alternative way to both this issue and this issue, with a different approach. This suggestion completely unifies literal suffixes with constructors instead of integrating them with templates.
Edits