Chapter 14

C-Style Programming

Images

Cippi mixes C with C++ code.

Due to the shared history of C and C++, both languages are closely related. Because neither of them is a subset of the other, you have to know a few rules to mix them.

This section in the C++ Core Guidelines consists of three rules. These three rules cover the typical issues encountered when dealing with legacy code.

CPL.1

Prefer C++ to C

Without further ado, here is the reason the C++ Core Guidelines prefer C++: “C++ provides better type checking and more notational support [than C]. It provides better support for high-level programming and often generates faster code.”

CPL.2

If you must use C, use the common subset of C and C++, and compile the C code as C++

The first question that you have to answer when mixing C and C++ is, Can you compile the entire code base with a C++ compiler?

Entire source code available

If the entire source code is available, you are almost done. I say “almost” because C is not a subset of C++. Here is a small and bad C program that breaks with a C++ compiler.

// cStyle.c
 
#include <stdio.h>
 
int main() {
 
    double sq2 = sqrt(2);                            // (1)
 
    printf("\nsizeof(\'a\'): %d\n\n", sizeof('a'));  // (2)
 
    char c;
    void* pv = &c;
    int* pi = pv;                                   // (3)

    int class = 5;                                  // (4)

}

First, let me compile the program and execute it with the C90 standard. The compilation succeeds with a few warnings (see Figure 14.1).

Images

Figure 14.1 Warnings with a C compiler

The program cStyle.c has a few issues. There is no declaration for the sqrt function (1), (3) performs an implicit conversion from a void pointer to an int pointer, and (4) uses the class keyword.

Let’s see how the C++ compiler reacts to the same code. See Figure 14.2.

Images

Figure 14.2 Errors with a C++ compiler

I get what I deserve: three compiler errors. The program cStyle.c shows more subtle differences between a C and a C++ compiler. Figure 14.3 shows the program reduced (2): printf("\nsizeof(\'a\'): %d\n\n", sizeof('a'));. Here is the output.

Images

Figure 14.3 Different size of a char with a C++ compiler

Instead of 4, such as for the C compiler, sizeof('a') is 1 with the C++ compiler. 'a' is an int in C.

Now, let’s discuss the more challenging case where the entire source code is not available.

Entire source code not available

These are the essential points.

  1. Use your C++ compiler to compile your main function. In contrast to a C compiler, a C++ compiler generates additional startup code that is executed before the main function. For example, the startup code calls constructors of global (static) objects.

  2. Use your C++ compiler to link your program. The C++ compiler, when used for linking the program, automatically links in standard C++ libraries.

  3. Use a C and C++ compiler from the same vendor, which should have the same calling conventions. A calling convention specifies the method that a compiler sets up to access a function. This includes in which order parameters are allocated, how parameters are passed, or whether the caller or the callee prepares the stack. Read the full details of x86’s calling conventions on Wikipedia (https://en.wikipedia.org/wiki/X86_calling_conventions).

CPL.3

If you must use C for interfaces, use C++ in the calling code using such interfaces

In contrast to C, C++ supports function overloading. This means that you can define functions having the same name but different parameters. The compiler picks the right function when a function is invoked.

// functionOverloading.cpp
 
#include <iostream> 
 
void print(int) { 
    std::cout << "int" << '\n'; 
} 
 
void print(double) { 
    std::cout << "double" << '\n'; 
} 
 
void print(const char*) { 


    std::cout << "const char* " << '\n'; 
} 
 
void print(int, double, const char*) { 
    std::cout << "int, double, const char* " << '\n'; 
} 
 
 
int main() { 
 
   std::cout << '\n'; 
 
   print(10); 
   print(10.10); 
   print("ten"); 
   print(10, 10.10, "ten");
 
   std::cout << '\n';
 
}

The output is as expected (see Figure 14.4).

Images

Figure 14.4 Function overloading

The interesting question is now: How can the C++ compiler distinguish the various functions? The C++ compiler additionally encodes the type and numbers of the parameters into the function name. This process is called “name mangling” and is specific for each C++ compiler. The process, which is not standardized, is often also called “name decoration.”

With the help of the functionOverloading.cpp on Compiler Explorer, it is quite easy to see the mangled names. Just disable the button Demangle.

Table 14.1 shows the names that the GCC 8.3 and the MSVC 19.16 are producing.

Table 14.1 Name mangling

Function

GCC 8.3

MSVC 19.16

print(int)

_Z5printi

?print@@YAXH@Z

print(double)

_Z5printd

?print@@YAXN@Z

print(const char*)

_Z5printPKc

?print@@YAXPEBD@Z

print(int, double, const char*)

_Z5printidPKc

?print@@YAXHNPEBD@Z

By using the extern "C" linkage specifier, you can prevent the C++ compiler from mangling the names. The result is that you can call a C function from C++ or a C++ function from C.

You can use extern "C" for

  • Each function.

    extern "C" void foo(int);
  • Each function in a scope.

    extern "C" {
       void foo(int);
       double bar(double);
    }
  • The entire header file by using include guards. The macro __cplusplus is defined when the C++ compiler is used.

    #ifdef __cplusplus
    extern "C" {
    #endif
        void foo(int);
        double bar(double);
        .
        .
        .
    #ifdef __cplusplus
    }
    #endif