1 of 158

Modern C

and what we can learn from it

By Luca Sas

2 of 158

Modern C?

3 of 158

Modern C?

  • What if there is more to C than meets the eye?

4 of 158

5 of 158

Goals

6 of 158

Goals

  • Present a modern way in which C can be used.

7 of 158

Goals

  • Present a modern way in which C can be used.�
  • Explore how ideas inspired from C can improve our C++ code.

8 of 158

Goals

  • Present a modern way in which C can be used.�
  • Explore how ideas inspired from C can improve our C++ code.�
  • Showcase how these ideas manifest in other languages, old and new.

9 of 158

About me

Core Systems Engineer @ Creative Assembly

Game Development

Low Level Systems

Programming Languages

BananyaDev#0001

@SasLuca

sas.luca.alex@gmail.com

@

10 of 158

A refresh on C

11 of 158

A refresh on C

  • Developed in 1972 (49 years ago)

12 of 158

A refresh on C

  • Developed in 1972 (49 years ago)�
  • Standardized by ANSI and later by ISO (ANSI C, C89, C99, C11, …)

13 of 158

A refresh on C

  • Developed in 1972 (49 years ago)�
  • Standardized by ANSI and later by ISO (ANSI C, C89, C99, C11, …)�
  • C does still have an active ISO committee (WG14)

14 of 158

A refresh on C

15 of 158

A refresh on C

  • Developed in 1972 (49 years ago)�
  • Standardized by ANSI and later by ISO (ANSI C, C89, C99, C11, …)�
  • C does still have an active ISO committee (WG14)�
  • What have we missed in the past 50 years and how is C different from C++?

16 of 158

17 of 158

18 of 158

19 of 158

Variables and structs

20 of 158

Variables and structs

21 of 158

Primitive types

22 of 158

Primitive types

23 of 158

Primitive types

24 of 158

Functions

25 of 158

C++ is not C

  • C++ is not fully compatible with C.

26 of 158

C++ is not C

  • C++ is not fully compatible with C.�
  • They are compatible enough that C headers will mostly work in C++.

27 of 158

C++ is not C

  • C++ is not fully compatible with C.�
  • They are compatible enough that C headers will mostly work in C++.�
  • C++ did inherit a lot of baggage from this attempted compatibility with C.

28 of 158

What else is different between C and C++?

29 of 158

Struct initialization

30 of 158

Struct initialization

  • Unlike C++, structs in C are just plain old bags of data (PODs)

31 of 158

Struct initialization

  • Unlike C++, structs in C are just plain old bags of data (PODs)�
  • C99 introduces a new and powerful form of struct initialization.

32 of 158

Struct initialization

33 of 158

Struct initialization

34 of 158

Struct initialization

35 of 158

Struct initialization

36 of 158

Struct initialization

37 of 158

Struct initialization

38 of 158

Struct initialization

39 of 158

Struct initialization

40 of 158

Struct initialization

41 of 158

Struct initialization

42 of 158

Struct initialization

43 of 158

Struct initialization

44 of 158

Struct initialization

  • Unlike C++, structs in C are just plain old bags of data (PODs)�
  • C99 introduces a new and powerful form of struct initialization.�
  • We can easily initialize complex data structures with nested initializers.

45 of 158

Struct initialization

  • Unlike C++, structs in C are just plain old bags of data (PODs)�
  • C99 introduces a new and powerful form of struct initialization.�
  • We can easily initialize complex data structures with nested initializers.�
  • Everything we don’t explicitly initialize gets set to 0 (ZII).

46 of 158

Struct initialization

  • Unlike C++, structs in C are just plain old bags of data (PODs)�
  • C99 introduces a new and powerful form of struct initialization.�
  • We can easily initialize complex data structures with nested initializers.�
  • Everything we don’t explicitly initialize gets set to 0 (ZII).�
  • This enabled more declarative and modern styles of programing.

47 of 158

Struct initialization

  • Unlike C++, structs in C are just plain old bags of data (PODs)�
  • C99 introduces a new and powerful form of struct initialization.�
  • We can easily initialize complex data structures with nested initializers.�
  • Everything we don’t explicitly initialize gets set to 0 (ZII).�
  • This enabled more declarative and modern styles of programing.�
  • C++ finally caught up after 20 years.

48 of 158

Struct initialization: sokol gfx

49 of 158

Struct initialization: sokol gfx

50 of 158

What Modern C is

  • Challenging the way APIs have historically been structured in C.

51 of 158

What Modern C is

  • Challenging the way APIs have historically been structured in C.�
  • Using newer features of C in order to improve the ease of use and safety of the API.

52 of 158

What Modern C is

  • Challenging the way APIs have historically been structured in C.�
  • Using newer features of C in order to improve the ease of use and safety of the API.
  • Decoupling the data manipulations from hosted services (allocators, io).

53 of 158

What Modern C is

  • Challenging the way APIs have historically been structured in C.�
  • Using newer features of C in order to improve the ease of use and safety of the API.
  • Designing systems with portability and ease-of-use in mind. (Allocators, centralizing resources, separating hosted code from freehosted code)

54 of 158

C11 Additions

  • The latest major version.

55 of 158

C11 Additions

  • The latest major version.�
  • As of recent it is now supported by all major compilers including MSVC.

56 of 158

C11 Additions: static assert

57 of 158

C11 Additions: _Generic and overloading

58 of 158

C11 Additions: Atomic operations

59 of 158

C11 Additions: thread_local

60 of 158

C11 Additions: thread_local

61 of 158

C11 Additions

  • The latest major version.�
  • As of recent it is now supported by all major compilers including MSVC.�
  • Static_assert.�
  • Overloading.�
  • atomics (available in older versions with libraries like c89atomics).�
  • thread_local (available via compiler extensions in C99).

62 of 158

Awesome Macros

63 of 158

Awesome Macros

  • Macros CAN be evil!

64 of 158

Awesome Macros

  • Macros CAN be evil!�
  • There are some cases in which they can make our life much nicer.

65 of 158

Awesome Macros: defer

66 of 158

Awesome Macros: defer

67 of 158

Awesome Macros: defer

68 of 158

Awesome Macros: defer

69 of 158

Awesome Macros: defer

70 of 158

Awesome Macros: defer

71 of 158

API Design

72 of 158

API Design: Math

73 of 158

API Design: Math

- Out params are harder to spot at the call site

74 of 158

API Design: Math

- Out params are harder to spot at the call site

- There are indirection and aliasing issues caused by the pointers which can harm performance

75 of 158

API Design: Math in Modern C

- value oriented design

76 of 158

API Design: Math in Modern C

- value oriented design

- we can use literals in the function call and readability is improved

77 of 158

API Design: Math in Modern C

- value oriented design

- we can use literals in the function call and readability is improved

- performance is actually better

78 of 158

API Design: Math in Modern C

79 of 158

API Design: Error handling

80 of 158

API Design: Error handling

81 of 158

API Design: Error handling

82 of 158

API Design: Error handling

83 of 158

API Design: Error handling in Modern C

84 of 158

Error handling in Modern C

85 of 158

Error handling in Modern C

86 of 158

Error handling in Modern C

87 of 158

Error handling in C++ with optional

88 of 158

Error handling in Modern C

89 of 158

Error handling in Modern C

90 of 158

Error handling in Modern C

91 of 158

Error handling: std::expected

92 of 158

Error handling in Rust

93 of 158

Error handling in Zig

94 of 158

Error handling in C++

95 of 158

Generic APIs in C

  • C lacks generic programming (eg: templates)

96 of 158

Generic APIs in C

  • C lacks generic programming (eg: templates).�
  • C can also force us to think outside the box and not rely excessively on templates.

97 of 158

Generic APIs in C

  • C lacks generic programming (eg: templates).�
  • C can also force us to think outside the box and not rely excessively on templates.�
  • There do exist some solutions for some very common use cases.

98 of 158

Generic APIs in C: Trees and Graphs

99 of 158

Generic APIs in C: Trees and Graphs

100 of 158

Generic APIs in C: Dynamic Arrays

101 of 158

Generic APIs in C: Dynamic Arrays

102 of 158

Generic APIs in C: Dynamic Arrays

103 of 158

Generic APIs in C: Dynamic Arrays

104 of 158

Generic APIs in C: Map

https://github.com/nothings/stb/blob/master/stb_ds.h

105 of 158

Libraries in C

  • Historically messy due to the multitude of build systems and no standard.

106 of 158

Libraries in C

  • Historically messy due to the multitude of build systems and no standard.�
  • Single header libraries arise as a convenient way to solve this problem.

107 of 158

Libraries in C

  • Historically messy due to the multitude of build systems and no standard.�
  • Single header libraries arise as a convenient way to solve this problem.

108 of 158

Libraries in C

  • Historically messy due to the multitude of build systems and no standard.
  • Single header libraries arise as a convenient way to solve this problem.

  • Sane CMake is another good option. Single headers can optionally be auto-generated.

109 of 158

Great talk on Cmake

110 of 158

Libraries in newer languages

  • Newer langs improve significantly on the library situation.�
  • Rust has a very well received easy to use build system and centralized database for libraries.�
  • Zig takes a unique approach by having the build system be part of the language.

111 of 158

When writing libraries

  • Avoid allocations if possible, request allocators or buffers from the user.

112 of 158

When writing libraries

  • Avoid allocations if possible, request allocators or buffers from the user.�
  • Try and make your library freehosted.

113 of 158

When writing libraries

  • Avoid allocations if possible, request allocators or buffers from the user.�
  • Try and make your library freehosted.

114 of 158

Memory management

  • Consider centralizing allocations.�
  • Differentiate between temporary and long lived allocations.�
  • Use buffers with maximum sizes where possible.�
  • Consider handles instead of pointers (eg: ECS)

115 of 158

Temporary allocators

116 of 158

Temporary allocators

117 of 158

Temporary allocators

118 of 158

API Design: Modern C

  • More declarative and functional, less stateful.�
  • More value oriented and less pointers.�
  • Zero initialization is used heavily (ZII)�
  • Uses C99 and C11 features and macros to modernize the code.�
  • Centralized resource management.�
  • Allocator aware.

119 of 158

String handling in C

  • Strings in C have been a historical disaster.

120 of 158

String handling in C

  • Strings in C have been a historical disaster.�
  • Terrible standard API (strstr, strtok, strpbrk).

121 of 158

Public service announcement: avoid libc

  • Very old�
  • Slow�
  • Terrible API design�
  • Very few parts are actually useful (stdint.h, memmove/memcpy/memset, math.h)

122 of 158

Replacing libc functionality: printf

123 of 158

String handling in C

  • Strings in C have been a historical disaster.�
  • Terrible standard API (strstr, strtok, strpbrk).�
  • Null terminated strings are very slow.

124 of 158

125 of 158

126 of 158

It’s not just null terminated strings

127 of 158

Case study: std::string

Intent: reading an array of bytes representing text

128 of 158

Case study: std::string

129 of 158

What’s the problem with a few couple of temporary strings here and there?

130 of 158

Real life examples:�Chrome

  • Half of all allocations are std::string.

131 of 158

Real life examples:�Chrome

  • Half of all allocations are std::string.

  • Typing one character in the Omnibox used to result in over 25000 allocations.

132 of 158

133 of 158

Real life examples:�Chrome

  • Half of all allocations are std::string.

  • Typing one character in the Omnibox used to result in over 25000 allocations.

  • Performance issues resulted from lot’s of copies and temporary objects.

134 of 158

String handling in Modern C

  • No implicit conversions and constructors.

135 of 158

String handling in Modern C

  • No implicit conversions and constructors.�
  • Stronger distinction between owning and non-owning strings.

136 of 158

String handling in Modern C

137 of 158

String handling in Modern C

138 of 158

String handling in Modern C

139 of 158

String handling in Modern C

140 of 158

String handling in Modern C

141 of 158

String handling in Old C

142 of 158

String handling in Old C

143 of 158

String handling in Modern C

144 of 158

String handling in Modern C: overloading

145 of 158

String handling in Modern C: overloading

146 of 158

String handling in Modern C: another awesome macro

147 of 158

String handling in Modern C: another awesome macro

148 of 158

First optimization at CA

  • First ever optimization I did at Creative Assembly was related to string operations.

149 of 158

First optimization at CA

  • First ever optimization I did at Creative Assembly was related to string operations.�
  • A seemingly cheap function was splitting some string.

150 of 158

First optimization at CA

  • First ever optimization I did at Creative Assembly was related to string operations.�
  • A seemingly cheap function was splitting some string.�
  • The function didn’t seem to take much time unless you analyzed its cost cumulatively.

151 of 158

First optimization at CA

  • First ever optimization I did at Creative Assembly was related to string operations.�
  • A seemingly cheap function was splitting some string.�
  • The function didn’t seem to take much time unless you analyzed its cost cumulatively.�
  • The solution was to move to a non-allocating, lazily evaluated string splitter using std::string_view.

152 of 158

String handling in other languages

  • Rust has a built-in str type which is different from the owning String class.�
  • Zig and Odin have built-in array-view types and string-view manipulation functions.�
  • Java and C# also follow a similar model.

153 of 158

String handling in C++

  • Consider starting to use std::string_view instead of const std::string& in C++.�
  • C++17 added std::string_view.�
  • C++20 ranges will make lazy non-allocating algorithms more easy to use.

154 of 158

In conclusion

  • I hope that you gained a better appreciation for what can be done in a limited language like C.

155 of 158

In conclusion

  • I hope that you gained a better appreciation for what can be done in a limited language like C.�
  • Checkout the latest developments in C, C++ and also other systems languages.

156 of 158

In conclusion

  • I hope that you gained a better appreciation for what can be done in a limited language like C.�
  • Checkout the latest developments in C, C++ and also other systems languages.�
  • Re-examine some of the patterns and preconceptions that you may hold.

157 of 158

In conclusion

  • I hope that you gained a better appreciation for what can be done in a limited language like C.�
  • Checkout the latest developments in C, C++ and also other systems languages.�
  • Re-examine some of the patterns and preconceptions that you may hold.�
  • By challenging our current understanding and not taking things for granted we can discover new and better ways of doing things.

158 of 158

Shoutouts