-
Notifications
You must be signed in to change notification settings - Fork 265
Description
1. Preface
I'm looking for a feature to pass an expression itself like macro parameters instead of immediately calculate the result of an expression to pass its result to functions. To have this feature in Cpp2, the best condidate is Metaclass Functions with @name
, because it generates code. So @name expr
will generate new code from expr
, and it can be applied to declarations, null statements, block statements or parameterized block statements. It's all about Reflections and Generations.
2. Suggestion Detail
This suggestion heavely depends on @hsutter's P0707 paper about Metaclasses.
The idea is to allow Meta Functions to get a parameter, in addition to make its usage general. This suggestion requires chaining (args)class
syntax for object construction as described in this suggestion (e.g. (args)type1(args)type2(args)type3(args)...
). It would have the following use cases:
- User-defined Language Constructs: It is similar to macros in Cpp1:
@check (i < 10) every (1min) { //statements... }
- Attributes: It can be used as attribute specifier sequence in Cpp1:
func: () @noreturn = { exit(); }
- Contracts: It can be used as contracts proposals in Cpp1 (see NOTE 1):
func: (a: int, b: int) -> int @pre a < b = { /*statements*/ }
- Coroutines: It can be used as coroutines in Cpp1 (via
co_await
,co_yield
andco_return
):x: = @co_await call();
NOTE 1:
Contracts are a way to have preconditions, postconditions and assertions on functions. Mainly two different syntax is proposed to support them in Cpp1:
- Closure-based syntax:
func: (a: int, b: int) -> int pre {a < b} = { /*statements*/ }
- Attribute-like syntax:
func: (a: int, b: int) -> int [[pre: a < b]] = { /*statements*/ }
This suggestion is just a feature to explore, and I know that it isn't going to be implemented anytime soon as Reflection isn't ready yet in Cpp2.
The rules of the suggestion are that:
- Meta Functions can either have only one parameter or have not any parameter.
In the last line,
x: int @unused = 2; y: int @deprecated "reason" = 2; z: int @what (x) less (y) = 2;
(x) less (y)
is an expression which creates an object with(x) less
and callsoperator()
on it with(y)
. It is the only one parameter. - Meta Functions can be applied to declarations, null statements, block statements or parameterized block statements.
// declaration func: () @deprecated = { /*statements*/ } // null statement @assume something > 10; // block statement (copy i: int = 0) @whilee (i < 10) nextt (i++) { /*statements*/ } // parameterized block statement @forr list doo (items) { /*statements*/ }
- Multiple Meta Functions can be applied. They must be written in a sequence.
func: (a: int, b: int) -> (r: int) @pre a < b @post a < r < b @nodiscard "reason" @another = { /*statements*/ }
- They can be inside namespaces. For example
std::meta
is applied to a null statement:@std::meta "text";
Multiple parameters are not needed if we use reflections to parse that one parameter. After that we would generate new code based on that one parameter. Literally any expression are allowed to be that one parameter. Types look like they are keywords (althouth they are not) within chained object construction with (args)class
syntax (as described in this suggestion):
// function, class1 and class2 look like they are keywords but they are not.
@meta function (args) class1 (args) class2 (args)...
// class1 and class2 look like they are keywords but they are not.
@meta (args) class1 (args) class2 (args)...
The point is that a parameterized meta function would generate new code from its parameter.
For example:
(copy i: int = 0) @repeat iff (i < 10) nextt (i++) every (1min) {
print("Hello");
}
The above code would generate this new code:
(copy i: int = 0, copy __t: timer = ()) while i < 10 next i++ {
print("Hello");
__t.delay(1min);
}
Use Cases
2.1. User-defined Language Constructs
They are similar to macros in C and Cpp1, except:
- They are safe. They are integrated into Cpp2 syntax.
- They are readable. They are distinguishable from other identifiers with
@
prefix. - They cannot mutate the language. They cannot be redefined. They may be in namespaces.
User-defined Language Constructs are Meta Functions which would be applied to null statements, block statements or parameterized block statements. Cpp1 attributes in which they apply to null statements, are somehow language constructs. For example, [[assume(expr)]];
and [[fallthrough]];
attributes in Cpp1, would be like this with the suggestion:
//[[assume(something > 0)]];
@assume something > 0;
// If Cpp2 has switch-case control structure:
switch (n) {
case 1:
// [[fallthrough]];
@fallthrough;
case 2:
// [[fallthrough]];
@fallthrough;
case 3:
call();
}
Also when the resource have to be released, we can have a user-defined language construct for it:
@using resource {
//statements...
// It would release the resource with `resource.close()`.
}
2.2. Attributes
Parameterized meta functions are similar to attributes in Cpp1, except they are user-defined as library features.
This is how to use them to change the compiler behaviour or to inform the compiler to how generate code. For example, this is equal to [[noreturn]]
in Cpp1:
// This function never ends!
function: () @noreturn = {
while true {
std::cout << "Forever you see this message again!\n";
}
}
Compilers can implement their own attributes in Cpp2 as library features instead of how they have implementation-defined attributes in Cpp1. For example, alias
extension attribute in GCC:
#include <compiler>
__func: (params) -> int = {
/// statements...
}
func: (params) -> int @weak @alias "__f";
In this case, they do not generate any statement, they only change the compiler behaviour.
The compiler could provide its options in compiler
object:
compiler.set("func", "weak");
compiler.set("func", "alias", "__f");
Meta functions can change the compiler behaviour with compiler
object (reflections API).
2.3 Contracts
Contracts pre
and post
are meta functions. They would add test conditions to the before and the after of function body. They generate new function body if that's necessary. For example:
func: (a: int, b: int) -> (r: int) @pre a < b @post a < r < b = {
//statements...
}
The above declaration will generate this declaration:
func: (a: int, b: int) -> (r: int) = {
assert(a < b);
//statements...
assert(a < r < b);
// Here it would return `r` after the above line.
}
2.4 Coroutines
Coroutines could be a library feature in Cpp1 too (via co_await
, co_yield
and co_return
). For example:
tcp_echo_server: () -> task<> = {
//statements...
while true {
n: std::size_t = @co_await socket.async_read_some(buffer(data));
@co_await async_write(socket, buffer(data, n));
}
}
iota: (n: int = 0) -> generator<int> = {
while true {
@co_yield n++;
}
}
func: () -> lazy<int> = {
@co_return 7;
}
The above examples are modified Cpp2 version from cppreference.com examples.
3. Your Questions
This suggestion won't eliminate X% of security vulnerabilities of a given kind in current C++ code.
But this suggestion will automate or eliminate X% of current C++ guidance literature. It would unify Contracts, Attributes, Coroutines and User-defined Language Constructs to a single concept Parameterized Meta Functions.
Also this suggestion would help compiler and library writers:
- Compilers need to provide their own extensions. Attributes in Cpp1 is a way for it.
- This suggestion makes them to be library feature.
- Libraries need to provide simpler use of their own interfaces. Macros in Cpp1 is a way for it.
- This suggestion is a safe integrated aliternative way to macros.
4. Considered Alternatives
I've considered to use named arguments instead of (args)class1(args)class2(args)...
, but the problem of named arguments is that they don't look like built-in language constructs, also they need comma:
@repeat list: = items, every: = 1min, do (arg) {
print("Hello");
}