Declarations and aliases
Unified declarations
All Cpp2 declarations are written as "name :
kind =
statement".
-
The name must be a valid identifier (start with a letter, and consist of letters, digits, or
_
). The name can be variadic (be a name for a list of zero or more things) by writing a...
suffix at the end of the name. -
The
:
is pronounced "is a." -
The kind can start with template parameters and end with
requires
constraints. -
The
=
is pronounced "defined as." For the definition of something that will always have the same value, write==
, pronounced "defined as a synonym for". -
The statement is typically an expression statement (e.g.,
a + b();
) or a compound statement (e.g.,{ /*...*/ return c(d) / e; }
).
Various parts of the syntax allow a _
"don't care" wildcard or can be omitted entirely to accept a default (e.g., x: int = 0;
can be equivalently written x: _ = 0;
or x := 0;
both of which deduce the type).
Notes:
When the type is omitted, whitespace does not matter, and writing
x: = 0;
orx : = 0;
orx := 0;
or other whitespace is just a stylistic choice. This documentation's style uses the last one, except when there are multiple adjacent declaration lines this style lines up their:
and=
.
==
stresses that this name will always have the given value, to express aliases and side-effect-free 'constexpr' function aliases.
Unnamed declaration expressions
In an expression, most declarations can be written without a name (just starting with :
). Such unnamed declaration expressions are useful for single-use temporary variables or 'lambda' functions that don't need a name to be reused elsewhere. For example:
-
:widget = 42
is an unnamed expression-local (aka temporary) object of typewidget
defined as having the initial value42
. It uses the same general syntax, just without declaring a name. -
:(x) = std::cout << x
is an unnamed expression-local generic function expression (aka lambda) defined as having the given one-statement body. The body can include captures.
Both just omit the name and make the final ;
optional. Otherwise, they have the identical syntax and meaning as if you declared the same thing with a name outside expression scope (e.g., w: widget = 42;
or f: (x) = std::cout << x;
) and then used the name in the expression.
Note: Throughout Cpp2, every declaration is written with
:
, and every use of:
is a declaration.
From functions to local scopes, and back again
The function syntax is deliberately designed to be general, so you can omit parts. This means Cpp2 has no special "lambda function" syntax for unnamed functions; an unnamed function is really an unnamed function, written using the ordinary function just without a name. This scales all the way down to ordinary blocks and statements, which are written the same as functions that have no name or parameters.
We can illustrate this in two directions. First, let's start with a full function, and successively omit optional parts that we aren't currently using:
// Full named function
f:(x: int = init) = { /*...*/ } // x is a parameter to the function
f:(x: int = init) = statement; // same, except return type is deduced
// Omit name => anonymous function (aka 'lambda')
:(x: int = init) = { /*...*/ } // x is a parameter to the function
:(x: int = init) = statement; // same, except return type is deduced
// Omit declaration => local and immediate (aka 'let' in other languages)
(x: int = init) { /*...*/ } // x is a parameter to this
(x: int = init) statement; // compound or single-statement
// Omit parameters => ordinary block or statement
{ /*...*/ } // ordinary compound statement
statement; // ordinary statement
Conversely, we can start with an ordinary block or statement, and successively build it up to make it more powerful:
// Ordinary block or statement
{ /*...*/ } // ordinary compound statement
statement; // ordinary statement
// Add parameters => more RAII locally-scoped variables
(x: int = init) { /*...*/ } // x is destroyed after this
(x: int = init) statement; // compound or single-statement
// Add declaration => treat the code as a callable object
:(x: int = init) = { /*...*/ } // x is a parameter to the function
:(x: int = init) = statement; // same, except return type is deduced
// Add name => full named function
f:(x: int = init) = { /*...*/ } // x is a parameter to the function
f:(x: int = init) = statement; // same, except return type is deduced
Template parameters
A template parameter list is a list enclosed by <
>
angle brackets, and the parameters separated by commas. Each parameter is declared using the same syntax as any type or object. If a parameter's :
kind is not specified, the default is : type
.
For example:
array: <T: type, size: i32> type
// parameter T is a type
// parameter size is a 32-bit int
= {
// ...
}
tuple: <Ts...: type> type
// parameter Ts is variadic list of zero or more types
= {
// ...
}
requires
constraints
A requires
condition constraint appears at the end of the kind of a templated declaration. If the condition evaluates to false
, that specialization of the template is ignored as if not declared.
For example:
print: <Args...: type>
(inout out: std::ostream, args...: Args)
requires sizeof...(Args) >= 1u
= {
(out << ... << args);
}
Examples
Note:
@enum
is a metafunction, which provides an easy way to opt into a group of defaults, constraints, and generated functions. For details, see@enum
.
Aliases
Aliases are pronounced "synonym for", and written using the same name :
kind =
value declaration syntax as everything in Cpp2:
-
name is declared to be a synonym for value.
-
kind can be any of the kinds:
namespace
,type
, a function signature, or a type. -
==
, pronounced "defined as a synonym for", always precedes the value. The==
syntax stresses that during compilation every use of the name could be equivalently replaced with the value. -
value is the expression that the name is a synonym for.
Namespace aliases
A namespace alias is written the same way as a namespace, but using ==
and with the name of another namespace as its value. For example:
// 'chr' is a namespace defined as a synonym for 'std::chrono'
chr : namespace == std::chrono;
// 'chrlit' is a namespace defined as a synonym for 'std::chrono_literals'
chrlit : namespace == std::chrono_literals;
main: () = {
using chrlit::_ ;
// The next two lines are equivalent
std::cout << "1s is (std::chrono::nanoseconds(1s).count())$ns\n";
std::cout << "1s is (chr::nanoseconds(1s).count())$ns\n";
}
// Prints:
// 1s is 1000000000ns
// 1s is 1000000000ns
Type aliases
A type alias is written the same way as a type, but using ==
and with the name of another type as its value. For example:
// 'imap<T>' is a type defined as a synonym for 'std::map<i32, T>'
imap : <T> type == std::map<i32, T>;
main: () = {
// The next two lines declare two objects with identical type
map1: std::map<i32, std::string> = ();
map2: imap<std::string> = ();
// Assertion they are the same type, using the same_as concept
static_assert( std::same_as< decltype(map1), decltype(map2) > );
}
Function aliases
A function alias is written the same way as a function, but using ==
and with a side-effect-free body as its value; the body must always return the same value for the same input arguments. For example:
// 'square' is a function defined as a synonym for the value of 'i * i'
square: (i: i32) -> _ == i * i;
main: () = {
// It can be used at compile time, with compile time values
ints: std::array<i32, square(4)> = ();
// Assertion that the size is the square of 4
static_assert( ints.size() == 16 );
// And it can be used at run time, with run time values
std::cout << "the square of 4 is (square(4))$\n";
}
// Prints:
// the square of 4 is 16
Note: A function alias is compiled to a Cpp1
constexpr
function.
Object aliases
An object alias is written the same way as an object, but using ==
and with a side-effect-free value. For example:
// 'BufferSize' is an object defined as a synonym for the value 1'000'000
BufferSize: i32 == 1'000'000;
main: () = {
buf: std::array<std::byte, BufferSize> = ();
static_assert( buf.size() == BufferSize );
}
Note: An object alias is compiled to a Cpp1
constexpr
object.