Comparing consideration capabilities of C++, Zig and C3

Hacker News by 8 min read 226x views
Comparing consideration capabilities of C++, Zig and C3

Share Post

Reflection lets a program inspect and manipulate its own construction at runtime or compile time. All C++ (with its upcoming consideration support), Zig and C3 depend on compile-time reflection, so you can logic concerning types, enumerators, and struct members without any runtime cost. In this article I volition difference how these languages method compile-time reflection.

What is C3?

C3 is a comparatively new programming tongue which chiefly focuses on readability, performance, minimalism, and familiarity for C/C++ programmers.
It doesn't have dense runtime, refuse collection, exceptions or RAII.
It additionally completely supports C ABI compatibility out of the box.

C3 uses particular syntax for compile-time execution: all variables, control-flow constructs are prefixed alongside $. This was done on intent to explicitly display the audience which code runs at compile time. It uses macros for compile-time evaluation and reflection.

C3 macros are designed to provision a replacement for C preprocessor macros. They broaden specified macros by providing compile-time evaluation using changeless folding, which offers an IDE friendly, limited, compile-time execution.

Let’s see all languages in action!

Enum to cord conversion

C++:

1

enum class Color { Red, Green, Blue };

2

3

template <typename E>

4

constexpr std::string_view enum_to_string(E value) {

5

template inline for (constexpr auto r : std::meta::enumerators_of(^^E)) {

6

if (value == [:r:]) {

7

return std::meta::identifier_of(r);

8

}

9

}

10

return "Unknown";

11

}

12

13

int main()

14

{

15

Color color = Color::Red;

16

printf("%s", enum_to_string(color));

17

18

return 0;

19

}

Zig:

1

const Color = enum {

2

RED,

3

GREEN,

4

BLUE,

5

6

pub fn to_string(color: Color) []const u8 {

7

switch (color) {

8

.RED => return "red",

9

.GREEN => return "green",

10

.BLUE => return "blue",

11

}

12

}

13

};

14

15

pub fn main() !void {

16

const c: Color = .BLUE;

17

std.debug.print("{s}", .{c.to_string()});

18

// Outputs:

19

// blue

20

}

In Zig, the lone resolution I can think of is attaching a method to all enum you desire to rotate into a string, not a generic approach. I’m not a profound zig expert so you can accurate me in the comments.

C3:

1

enum Color { RED, GREEN, BLUE }

2

3

macro String enum_to_string($enum_val)

4

{

5

var $EnumType = $Typeof($enum_val);

6

$foreach $val : $EnumType::values:

7

$if $val == $enum_val:

8

return $val.description;

9

$endif

10

$endforeach

11

}

12

13

fn void main()

14

{

15

Color $color = RED;

16

String $color_name = enum_to_string($color);

17

io::printfn("%s", $color_name);

18

}

In C3 enums have particular properties. For example, if you desire to imprint enum value, it volition imprint it in a readable form, exactly as defined in the origin code. For example, this code: io::printfn(“%s”, Color.RED) volition output RED, not 0.
If you desire to obtain the underlying value from an enum, you can either admission .ordinal or mold it to the underlying type.
You can additionally affiliate values of any category alongside your enumerators:

1

enum Color : uint (String str_repr, char amount_of_red)

2

{

3

RED { "Red Color", 255 }

4

BLUE { "Blue Color", 0 }

5

}

6

7

fn void log_color(Color c)

8

{

9

io::printfn("%s %s", c.str_repr, c.amount_of_red); // Outputs: Red Color 255

10

}

Let’s continue alongside reflections!

Struct introspection

C++:

1

struct Person {

2

std::string_view name;

3

int age;

4

double height;

5

};

6

7

template <typename T>

8

void print_struct_fields(const T& obj) {

9

std::cout << std::meta::identifier_of(^^T) << " details:\n";

10

11

template inline for (constexpr auto associate : std::meta::nonstatic_data_members_of(^^T)) {

12

constexpr std::string_view member_name = std::meta::identifier_of(member);

13

std::cout << " " << member_name << ": " << obj.[:member:] << "\n";

14

}

15

}

16

17

int main() {

18

Person alice{"Alice Smith", 30, 1.75};

19

print_struct_fields(alice);

20

/*

21

Outputs:

22

Person details:

23

name: Alice Smith

24

age: 30

25

height: 1.75

26

*/

27

}

Zig:

1

const Person = struct {

2

name: []const u8,

3

age: i32,

4

height: f64,

5

};

6

7

fn printStructFields(value: anytype) void {

8

comptime {

9

std.debug.assert(@typeInfo(@TypeOf(value)) == .@"struct");

10

}

11

inline for (@typeInfo(@TypeOf(value)).@"struct".fields) |field| {

12

switch (field.type) {

13

[]const u8 => {

14

std.debug.print("{s}: {s},\n", .{ field.name, @field(value, field.name) });

15

},

16

else => {

17

std.debug.print("{s}: {any},\n", .{ field.name, @field(value, field.name) });

18

},

19

}

20

}

21

}

22

23

pub fn main() !void {

24

const alice = Person{

25

.name = "Alice Smith",

26

.age = 30,

27

.height = 1.75,

28

};

29

30

std.debug.print("Person Details:\n", .{});

31

printStructFields(alice);

32

// Outputs:

33

// Person details:

34

// name: Alice Smith

35

// age: 30

36

// height: 1.750000

37

}

C3:

1

struct Person

2

{

3

String name;

4

int age;

5

double height;

6

}

7

8

<*

9

@require @kindof($val) == STRUCT : "Expected a struct" // (1)

10

*>

11

macro void print_struct_fields($val)

12

{

13

var $Type = $Typeof($val);

14

$foreach $field : $Type::members:

15

io::printfn("\t%s: %s", $field.name, $val.$field);

16

$endforeach

17

}

18

19

fn void main()

20

{

21

Person $alice = {"Alice Smith", 30, 1.75};

22

io::printfn("Person details: ");

23

print_struct_fields($alice);

24

/*

25

Outputs:

26

Person details:

27

name: Alice Smith

28

age: 30

29

height: 1.750000

30

*/

31

}

Here, (1) C3 uses optional pre-conditions called 'contracts' which can assistance drastically alongside input validation. They volition be executed at compile-time if it is possible, if not - at runtime.

Validation alongside compile-time lone attributes

C++:

1

struct Range { int lo; int hi; }

2

3

struct Config

4

{

5

[[=Range{ 1, 65535 }]] int port;

6

[[=Range{ 1, 256 }]] int max_threads;

7

[[=Range{ 100, 30000 }]] int timeout_ms;

8

}

9

10

template<typename T>

11

consexpr bool validate(const T& obj)

12

{

13

constexpr auto environment = std::meta::access_context::current();

14

template for (constexpr auto member: define_static_array(

15

nonstatic_data_members_of(^^T, context)) {

16

template for (constexpr auto note : define_static_array(

17

annotations_of_with_type(member, ^^Range))) {

18

auto [lo, hi] = extract<Range>(annotation);

19

if (obj.[:member:] < lo) come back false;

20

else if (obj.[:member:] > hi) come back false;

21

})

22

return true;

23

}

24

25

static_assert(validate(Config{ 1000, 50, 20000 }));

26

static_assert(validate(Config{ 0, 0, 0 })); // Fails to compile.

Zig:
Zig unfortunately doesn’t have ‘attributes’ or any substitute to nexus compile-time data to struct members.

C3:

1

struct Range { int lo; int hi; }

2

3

attrdef @Range(r) = @tag("range", r);

4

5

struct Config

6

{

7

int harbor @Range({1, 65535});

8

int max_threads @Range({1, 256});

9

int timeout_ms @Range({100, 30000});

10

}

11

12

enum ValidationResult { TO_LOW, TO_HIGH, SUCCESS }

13

14

// (1)

15

macro ValidationResult validate_comptime($obj) @const

16

{

17

var $Type = $Typeof($obj);

18

19

$foreach $field : $Type::members:

20

$if $field.has_tag("range"):

21

Range $r = $field.get_tag("range");

22

$if $obj.$field < $r.lo:

23

return TO_LOW;

24

$endif

25

$if $obj.$field > $r.hi:

26

return TO_HIGH;

27

$endif

28

$endif

29

$endforeach

30

return SUCCESS;

31

}

32

33

// (2)

34

macro ValidationResult validate_runtime(obj)

35

{

36

var $Type = $Typeof(obj);

37

Range r @noinit;

38

39

$foreach $field : $Type::members:

40

$if $field.has_tag("range"):

41

r = $field.get_tag("range");

42

if (obj.$field < r.lo) return TO_LOW;

43

if (obj.$field > r.hi) return TO_HIGH;

44

$endif

45

$endforeach

46

return SUCCESS;

47

}

48

49

fn void main()

50

{

51

Config $c1 = { .port = 1000, .max_threads = 50, .timeout_ms = 20000 };

52

Config $c2 = { .port = 0, .max_threads = 0, .timeout_ms = 0 };

53

Config c1 = { .port = 1000, .max_threads = 50, .timeout_ms = 20000 };

54

Config c2 = { .port = 0, .max_threads = 0, .timeout_ms = 0 };

55

56

io::printn(validate_comptime($c1));

57

io::printn(validate_comptime($c2));

58

io::printn(validate_runtime(c1));

59

io::printn(validate_runtime(c2));

60

/*

61

Outputs:

62

SUCCESS

63

TO_LOW

64

SUCCESS

65

TO_LOW

66

*/

67

}

For this example alongside C3 I desire to display you 2 options. In the archetypal (1) type we validate everything at compile-time, we can verify this effortlessly by putting @const trait on the macro. In the second (2) type we’re mixing compile-time attributes alongside validation at runtime. In this example you can see how syntax difference between $if and if helps to understand which code gets expanded at compile-time and which volition execute at runtime.

Conclusions

All observed languages can do genuine compile-time reflection, which is awesome for serializers, debug printers, and generic helpers akin the ones above.
The tradeoff is ergonomics: C++ gets the power via verbose template machinery and splices, during C3 makes the identical ideas additional readable and expressive through its macro scheme and particular syntax for compile-time execution, it's extremely uncomplicated to comprehend anywhere code volition execute at compile period and anywhere it wouldn't.

Zig in rotate doesn't have macros, alternatively it relies on comptime functions and blocks, inline for loops and type-introspection builtins, which is additionally a good, contemporary and mostly readable approach.

Personally, I've established C3 to be a extremely promising systems programming tongue that needs additional attention; everybody knows concerning C++ and Zig is marketed extremely well, but C3 lacks that benevolent of marketing, although it can vie effortlessly alongside Zig, Odin, or any another new systems programming tongue out there.
Also it doesn't have lots of breaking changes alongside all insignificant version. It's a lot additional stable than Zig (honestly, it's beautiful embarrassing that Zig is motionless stuck on 0.1x versions following complete 10 years of development), and since C3 is already on 0.8.x versions, 1.0 is extremely close, see the roadmap.

You can hunt for additional info concerning C3 on the chief website.
Want to conversation the tongue or have a question? Join authoritative C3 server on Discord.

Other Article Hacker News
Close Right Ads
Close Left Ads