Objects, initialization, and memory
Overview
An object can be declared at any scope: in a namespace, in a type
, in a function, in an expression.
Its declaration is written using the same name :
kind =
value declaration syntax as everything in Cpp2:
-
name starts with a letter and is followed by other letters, digits, or
_
. Examples:count
,skat_game
,Point2D
are valid names. -
kind is the object's type. In most places, except type scopes, you can write the
_
wildcard as the type (or omit the type entirely) to ask for the type to be deduced. When the type is a template, the templated arguments can be inferred from the constructor (via CTAD). -
value is the object's initial value. To use the default-constructed value, write
()
.
For example:
// numbers is an object of type std::vector<point2d>,
// defined as having the initial contents 1, 2, 3
numbers: std::vector<int> = (1, 2, 3);
numbers: std::vector = (1, 2, 3); // same, deducing the vector's type
// count is an object of type int, defined as having initial value -1
count: int = -1;
count: _ = -1; // same, deducing the object's type with the _ wildcard
count := -1; // same, deducing the object's type by just omitting it
// pi is a variable template; == signifies the value never changes (constexpr)
pi: <T: type> T == 3.14159'26535'89793'23846L;
pi: _ == 3.14159'26535'89793'23846L; // same, deducing the object's type
The object type can be deduced by writing _
(the default, so it can be omitted). You can use is
to declare a type constraint (e.g., a concept) that a deduced type must match, in which case _
is required. For example:
// number's type is deduced, but must match the std::regular concept
number: _ is std::regular = some_factory_function();
Guaranteed initialization
Every object must be initialized using =
before it is used.
An object in any scope can be initialized at its declaration. For example:
shape: type = {
// An object at type scope (data member)
// initialized with its type's default value
points: std::vector<point2d> = ();
draw: (this, where: canvas) -> bool
= {
// An object at function scope (local variable)
// initialized with color::red
pen := color::red;
// ...
}
// ...
}
Additionally, at function local scope an object obj
can be initialized separately from its declaration. This can be useful when the object must be declared before a program-meaningful initial value is known (to avoid a dead write of a wrong 'dummy' value), and/or when the object may be initialized in more than one way depending on other logic (e.g., by using different constructors on different paths). The way to do this is:
-
Declare
obj
without an= initializer
value, for example:obj: some_type;
. This allocates stack space for the object, but does not construct it. -
obj
must have a definite first use on everyif
/else
branch path (and that first use must not be inside any loop), and -
that definite first use must be of the form
obj = value;
which is a constructor call, or else passobj
as anout
argument to anout
parameter (which is also effectively a constructor call, and performs the construction in the callee).
For example:
f: () = {
buf: std::array<std::byte, 1024>; // uninitialized
// ... calculate some things ...
// ... no uses of buf here ...
buf = some_calculated_value; // constructs (not assigns) buf
// ...
std::cout << buf[0]; // ok, a has been initialized
}
g: () = {
buf: std::array<std::byte, 1024>; // uninitialized
if flip_coin_is_heads() {
if heads_default_is_available {
buf = copy_heads_default(); // constructs buf
}
else {
buf = (other, constructor); // constructs buf
}
}
else {
load_from_disk( out buf ); // constructs buf (*)
}
std::cout << buf[0]; // ok, a has been initialized
}
load_from_disk: (out x) = {
x = /* data read from disk */ ; // when `buffer` is uninitialized,
} // constructs it; otherwise, assigns
In the above example, note the simple rule for branches: The local variable must be initialized on both the if
and else
branches, or neither branch.
Heap objects
Objects can also be allocated on the heap using xxx.new <T> (/*initializer, arguments*/)
where xxx
is any object that acts as a memory allocator and provides a .new
function template. Two memory allocators objects are provided in namespace cpp2
:
-
unique.new<T>
callsstd::make_unique<T>
and returns astd::unique_ptr<T>
. -
shared.new<T>
callsstd::make_shared<T>
and returns astd::shared_ptr<T>
.
The default is unique.new
if you don't specify an allocator object.
For example (see types for more details about writing types):
f: () -> std::shared_ptr<widget>
= {
// Dynamically allocate an object owned by a std::unique_ptr
// 'vec' is a unique_ptr<vector<i32>> containing three values
vec := new<std::vector<i32>>(1, 2, 3);
// shorthand for 'unique.new<...>(...)'
std::cout << vec*.ssize(); // prints 3
// note that * dereference is a suffix operator
// Dynamically allocate an object with shared ownership
wid := cpp2::shared.new<widget>();
store_a_copy( wid ); // store a copy of 'wid' somewhere
return wid; // and move-return a copy too
} // as always in C++, vec is destroyed here automatically, which
// destroys the heap vector and deallocates its dynamic memory