Notes for Circle video tutorial #1
1
#include <cstdio>
@meta printf("Hello circle\n");
int main() {
return 0;
}
Whatever statement you put after the meta keyword is executed at compile time. For meta statements inside templates, the statement is parsed at definition and executed during instantiation.
2
#include <cstdio>
@meta for(int i = 0; i < 5; ++i)
@meta printf("Hello circle %d\n", i);
int main() {
return 0;
}
You can perform structured control at compile-time. The child statement is translated at each iteration. Here, the child printf statement is executed five times at compile time.
3
#include <cstdio>
@meta for(int i = 0; i < 5; ++i)
int x = i;
int main() {
return 0;
}
The child statement of the meta for is a real statement, so it gets injected in the innermost enclosing real scope, which in this case is the global namespace. Each loop iteration injects that object declaration: five object declarations means four redefinition errors.
To extend C++ for programmatic declarations, the @ operator translates strings and integers to identifiers. Identifiers are prepended with an underscore to make them valid identifiers.
#include <cstdio>
@meta for(int i = 0; i < 5; ++i)
int @(i) = i;
int main() {
return 0;
}
4
#include <cstdio>
@meta int count;
@meta printf("Enter array size: ");
@meta scanf("%d", &count);
double my_array[count];
@meta printf("declared array %s\n", @type_name(decltype(my_array)));
int main() {
return 0;
}
Circle has an integrated C++ interpreter, so we can perform unrestricted i/o with the system. We can generate types and functions from data, enabling a new data-driven imperative metaprogramming paradigm.
5
#include <cstdio>
template<typename type_t>
struct foo_t {
@meta printf("foo_t<%s>\n", @type_name(type_t));
};
int main() {
foo_t<int> a;
foo_t<char*> b;
return 0;
}
Meta statements can be put in any braced scope: namespace, class definition, enum definition or block scope. In dependent contexts, the meta statement is executed during instantiation.
#include <cstdio>
template<typename... types_t>
struct foo_t {
@meta int x[] { printf("%s\n", @type_name(types_t))... };
};
int main() {
foo_t<int, double, char*> obj;
return 0;
}
Meta stataments are compatible with template parameter packs. This is C++'s ridiculous way to call a function over a parameter pack. The pack expansion syntax is only allowed in function arguments and initializer lists, so here we create a compile-time integer array (which is a compile-time automatic variable, not a class member) and initialize it with the return object of the printf, expanded over each element of the types_t parameter pack.
#include <cstdio>
template<typename... types_t>
struct foo_t {
@meta for(int i = 0; i < sizeof...(types_t); ++i)
@meta printf("type %d is %s\n", i, types_t...[i]);
};
int main() {
foo_t<int, double, const char*> obj;
return 0;
}
Circle extends C++ with the ...[]
operator to directly index any parameter pack kind: type pack, non-type pack, template pack or function parameter pack. This allows imperative access to parameter packs. There are also Circle extensions to create parameter packs from several kinds of data.
#include <cstdio>
template<typename... types_t>
struct tuple_t {
@meta for(int i = 0; i < sizeof...(types_t); ++i)
types_t...[i] @(i);
};
int main() {
tuple_t<int, double, const char*> obj;
obj._0 = 1;
obj._1 = 3.14;
obj._2 = "Hello tuple";
return 0;
}
The Circle tuple is a flat data structure, with members given integer names and created straight from the indexed parameter pack.
6
We can use imperative metaprogramming to create types. Here we show how to use Circle's introspection keywords to generically access data inside types.
#include <iostream>
#include <type_traits>
template<typename... types_t>
struct tuple_t {
@meta for(int i = 0; i < sizeof...(types_t); ++i)
types_t...[i] @(i);
};
template<typename type_t>
void print_object(const type_t& obj) {
static_assert(std::is_class<type_t>::value,
"argument of print_object must be a class type");
@meta for(int i = 0; i < @member_count(type_t); ++i)
std::cout<< @member_name(type_t, i)<< ": "<< @member_ref(obj, i)<< "\n";
}
struct type2_t {
double x, y, z;
const char* s;
};
int main() {
tuple_t<int, double, const char*> obj;
obj._0 = 1;
obj._1 = 3.14;
obj._2 = "Hello tuple";
type2_t obj2 {
5, 10, 15, "Another type"
};
print_object(obj);
print_object(obj2);
return 0;
}
We can also introspect on the enumerators in an enumeration. This is useful for converting between strings and enumerator names.
#include <cstdio>
#include <type_traits>
#include <string>
enum my_enum_t {
a, b, c, d, e
};
template<typename type_t>
std::string enum_to_string(type_t e) {
static_assert(std::is_enum<type_t>::value,
"argument to enum_to_string must be enumeration");
switch(e) {
@meta for enum(auto e2 : type_t)
case e2:
return @enum_name(e2);
default:
return "<" + std::to_string(e) + ">";
}
}
int main() {
printf("%s\n", enum_to_string(my_enum_t::c).c_str());
printf("%s\n", enum_to_string((my_enum_t)15).c_str());
return 0;
}
7
#include <sstream>
#include <cstdio>
#include <type_traits>
#include <vector>
#include <map>
template<typename... types_t>
struct tuple_t {
@meta for(int i = 0; i < sizeof...(types_t); ++i)
types_t...[i] @(i);
};
template<typename type_t>
std::string enum_to_string(type_t e) {
static_assert(std::is_enum<type_t>::value,
"argument to enum_to_string must be enumeration");
switch(e) {
@meta for enum(auto e2 : type_t)
case e2:
return @enum_name(e2);
default:
return "<" + std::to_string(e) + ">";
}
}
template<typename type_t>
std::string print_object(const type_t& obj) {
std::ostringstream oss;
if constexpr(std::is_enum<type_t>::value) {
oss<< std::string(@type_name(type_t)) + "::" + enum_to_string(obj);
} else if constexpr(std::is_same<std::string, type_t>::value ||
std::is_same<const char*, type_t>::value) {
oss<< "\""<< obj<< "\"";
} else if constexpr(std::is_scalar<type_t>::value) {
oss<< obj;
} else if constexpr(std::is_array<type_t>::value ||
@is_class_template(type_t, std::vector)) {
// Print all array elements inside []
oss<< "[ ";
bool comma = false;
for(const auto& x : obj) {
if(comma) oss<< ", ";
oss<< print_object(x); // Recursive call to print items.
comma = true;
}
oss<< " ]";
return oss.str();
} else if constexpr(@is_class_template(type_t, std::map)) {
// Print all pairs inside {}
oss<< "{ ";
bool comma = false;
for(const auto& x : obj) {
if(comma) oss<< ", ";
oss<< print_object(x.first)<< " : "<< print_object(x.second);
comma = true;
}
oss<< " }";
} else {
static_assert(std::is_class<type_t>::value, "expected class type");
// Print all members inside {}
oss<< "{ ";
bool comma = false;
@meta for(int i = 0; i < @member_count(type_t); ++i) {
if(comma) oss<< ", ";
// Print key-value pair.
oss<< @member_name(type_t, i)<< " : ";
oss<< print_object(@member_ref(obj, i));
comma = true;
}
oss<< " }";
}
return oss.str();
}
int main() {
tuple_t<int, double[3], const char*, std::map<std::string, int> > obj {
1,
{ 101.1, 202.2, 303.3 },
"Hello tuple",
{ { "Hello", 100 }, { "World", 200 } }
};
printf("%s\n", print_object(obj).c_str());
return 0;
}