[blog] C++ for C programmers #1

C is everywhere on system-programming because it allows high-level abstraction keeping low-overhead. The strong type system is an example. It is used at compile time to check for program correctness. After compile time the types will have disappeared and the emitted assembly code will show no trace of the type system. C++ also takes zero-overhead abstraction and direct mapping to hardware very seriously. In fact, some C++ features incur even less overhead than the corresponding C code.

C++ compilers will deal with most of your C programming habits, so you can take advantage of the C++ tactical niceties while avoiding the language deeper themes. Even for seasoned C programmers, in this this way of coding you need to learn new concepts to take full advantage of it. In some resourceless environments (e.g., embedded software, some operating system kernels) the available toolchains have incomplete C++ support, and in such situations it’s possible to benefit from at least some C++ idioms. (LOSPINOSO, C++ Crash Course)

Function Overloading

In C, each function must have a unique name. But in C++ functions can share names as long as their arguments differ. It is called function overloading.

// Standard C library to convert an integral type to a C-style string

//integer to string
char* itoa(int value, char* str, int base);

//long integer to string
char* ltoa(long value, char* buffer, int base);

//unsigned long integer to string
char* ultoa(unsigned long value, char* buffer, int base);

// C++ library for the same purpose
//integer to string
char* toa(int value, char* str, int base);

//long integer to string
char* toa(long value, char* buffer, int base);

//unsigned long integer to string
char* toa(unsigned long value, char* buffer, int base);

// C++ Code using function overloading
int main(void) {
  char buff[10];
  int a = 1; 
  long b = 2; 
  unsigned long c = 3; 
  toa(a, buff, 10);
  toa(b, buff, 10);
  toa(c, buff, 10);
  return 0;
}

The constexpr

Much of the benefits in C++ come on compile-time. The keyword constexpr is an example. It tells the compiler that if possible to evaluate that expression on compile time, saving room on your flash memory if you are on embedded environment and avoiding macro functions.

constexpr int compute_tick_numbers(int time_in_ms) {
 /*code*/
}
constexpr tick_numbers = compute_tick_numbers(5);

References

Pointers are crucial on system programming. They enable you to handle large amounts of data efficiently by passing around data address instead of the actual data. In C++ you got additional features available that defend you against null dereferences and unintentional pointer reassignments.

You declare references with & rather than * and you interact with members using “.” rather than “->”.

#include <cstdio>

struct Foo {
	bool bar;
};

typedef struct Foo Foo_t;

void make_foo_ptr(Foo_t* data_struct); //takes a pointer to 
void make_foo_ref(Foo_t& data_struct); //takes a reference

void make_foo_ptr(Foo_t* data_struct) {
	//if (data_struct == nullptr) return; 
	data_struct->bar = true;
}
void make_foo_ref(Foo_t& data_struct) {
	data_struct.bar = true;
}

int main(void) {
	Foo_t foo_var{.bar = false}; //c++ style init;
	Foo_t& fooBar_ref = foo_var; //assign the variable name
	Foo_t* fooBar_ptr = &foo_var;
	make_foo_ref(fooBar_ref);
	make_foo_ptr(fooBar_ptr);
	return 0;
}

The compiler produces similar code. This is part of the assembly code generated on my x86-64, they are identical for both functions:

; 16   : void make_foo_ref(Foo_t& data_struct) {
; 17   : 	data_struct.bar = true;
	mov	rax, QWORD PTR data_struct$[rbp]
	mov	BYTE PTR [rax], 1

; 12   : void make_foo_ptr(Foo_t* data_struct) {
; 14   : 	data_struct->bar = true;
	mov	rax, QWORD PTR data_struct$[rbp]
	mov	BYTE PTR [rax], 1

References are safer than raw pointers. When dealing with pointers in C you usually need to add a safe-check like on line 13. For references, at compile time they cannot be null.

Not being null does not mean they cannot be pointing referencing to garbage. Take this function that returns a reference to a temporary variable:

Foo_t& func1(void) {
  Foo_t foo;
  return foo;
}

Using this function return will lead to undefined runtime behavior. foo is destroyed, and the return of Foo_t will be a dangling reference.

Another safety feature references got is that once they are initialized, they cannot be changed to point to another memory address. To appreciate this, look at two codes doing the same, in C and C++

//C-style
int main(void) {
   int a = 20;
   int* a_ptr = &a;
   int b = 40;
   *a_ptr = b; //a=40
   return 0;
}
//C++ 
int main(void) {
   int a = 20;
   int& a_ref = a;
   int b = 40;
   a_ref = b; //a=40 !!
   return 0;
}

When you put a reference on the left-side of an equal sign you are setting the pointed-to value equal to the right side.

auto Initialization, and implicit typedef of struct, union, and enum

With the auto keyword the compiler deduces the type on the right side and sets the variable type to the same. This can be very handy when dealing with function returns, when the return type changes later on, during a code refactoring. Also, typedefs for structs/unions/enums are implicitly created.

#include <cstdlib>

enum color { yellow, blue };
struct Camel {
 color camelColor;
};
Camel* make_camel(color camelColor) {
   Camel* camel = new Camel{ camelColor };
   return camel;
}
int main(void) {
   auto camel = make_camel(blue);
   free(camel);
   return 0;
}

Note the auto keyword is figuring out camel is a pointer to Camel type.

Use the enum class for type safety, since they cannot be implicitly converted to other types, like integers or another enum. Furthermore, you can explicitly tell the underlying type that makes the enumeration.

enum class State : unsigned char {
	IDLE = 0x00,
	RUNNING = 0x01,
	DONE = 0xFF
};
void init_machine(Machine* machine) {
	machine->current_state = State::IDLE;
    /*...*/
}

Namespaces

Namespaces allow you to declare scopes for the same identifiers. Let’s say you wanted to create a Foo structure and also a Foo function (not a good idea), but if you really wish:

#include <cstdio>
namespace DataStruc {
	struct Foo {
		int bar;
		bool var;
	};
}
namespace Func {
	void Foo(DataStruc::Foo* foo_arg) {
		foo_arg->bar = 100;
		foo_arg->var = false;
		printf("bar = %d, var = %d \n", foo_arg->bar, foo_arg->var);
	}
}
// terrible identifiers usage to show off namespaces  
int main(void) { 
  DataStruc::Foo Foo;
  Func::Foo(&Foo);
  return 0;
}

The main program output:

bar = 100, var = 0

There is no overhead when using namespaces and they are really useful in large projects to separate code in different libraries.

Intermingling C and C++ Object Files

On system programming is very common the need for a C compiler to link object files emitted by a C++ compiler (and vice-versa). Two issues are related to linking the files. First, the calling conventions in C and C++ could potentially be mismatched – the protocols for how the stack and registers are set when you call a function could be different. Second, C++ compilers emit different symbols than C to accommodate function overloading, namespaces and other C++ features.

C linkers know nothing about this decoration. The fix is to wrap the code you want to compile with C-style linkage using the statement extern “C”.

// header.h
#ifdef __cplusplus
extern "C" {
#endif
void foo(void);

struct bar {
  int count;
};
#ifdef __cplusplus
}
#endif

This header can be shared between C and C++ code. __cplusplus is a special identifier that the C++ compiler defines (but the C does not). So the C compiler would see just:

// header.h
void foo(void);
struct bar {
  int count;
};

While the C++ compiler sees:

// header.h
extern "C" {
  void foo(void);
  struct bar {
    int count;
  };
}

This text is based on Lospinoso’s book “C++ Crash Course”. The author calls this style of coding as “Super C”.

Author: Antonio Giacomelli de Oliveira

Embedded Systems Engineer

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: