Skip to content

Instantly share code, notes, and snippets.

@bradenbest
Created January 18, 2022 00:18
Show Gist options
  • Save bradenbest/a94df4a71c51104e3735dae119317cf9 to your computer and use it in GitHub Desktop.
Save bradenbest/a94df4a71c51104e3735dae119317cf9 to your computer and use it in GitHub Desktop.
C++ Killer
// cppkill - for when people won't stop attempting to compile your
// C code with a C++ compiler and complaining about compile errors
#ifndef CPPKILL_H
#define CPPKILL_H
#ifdef __cplusplus
#error This is C code, so please use a C compiler. Thanks :)
#endif
enum yes_this_is_valid_in_c_thank_you {
class, this, extends,
virtual, template, typename,
const_cast, static_cast, reinterpret_cast,
using, namespace, std, /* ;) */
boost, new, delete,
throw, try, catch,
mutable, requires, explicit
};
#endif
@bradenbest
Copy link
Author

image

@bradenbest
Copy link
Author

bradenbest commented Jan 18, 2022

In all seriousness, it's perfectly valid to mix C and C++ code, but there's a right way and a wrong way to do it.

The right way is to write the code separately and let the build system choose the compiler, making sure to wrap #includes of C headers in extern "C" { ... } in C++ files and to compile the object files together using the C++ compiler. Let the build system figure it out.

// foo.c
#include <stdlib.h>
#include "foo.h"

int *
foo ( size_t length )
{
    int *out = malloc(length * sizeof *out); // valid in C; not valid in C++

    if (out == NULL)
        goto exit_mallocfail;

    return out;

exit_mallocfail:
    {
        fprintf(stderr, "%s/%s: Failed to obtain memory.\n", __FILE__, __func__);
        return NULL;
    }
}

void
foo_free ( int *ptr )
{
    free(ptr);
}
// foo.h
#ifndef FOO_H
#define FOO_H

#include <stddef.h>

int * foo      (size_t length);
void  foo_free (int *ptr);

#endif // FOO_H
// bar.cpp
#include <iostream>

extern "C" {

#include "foo.h"

} // extern "C"

int
main ( void )
{
    int *array = foo(12);

    array[3] = 72;
    std::cout << array[3] << std::endl; // valid in C++; not valid in C
    foo_free(array);
    return 0;
}
# Makefile
CC = gcc
CXX = g++

default: proj

proj: foo.o bar.o
	$(CXX) $^ -o $@
$ make
gcc foo.c   -c -o foo.o
g++ bar.cpp -c -o bar.o
g++ foo.o bar.o -o proj
$ ./proj
72
$

The wrong way is to write code that attempts to satisfy both compilers at once. In order to do this, you have to restrict yourself to only the parts of C and C++ that happen to overlap, which is not very useful. You lose most of the functionality of both languages (believe it or not, C++ is not even remotely close to being a superset of C), as you have to use only parts that both overlap and behave the same. And on top of that, you'll have people like me criticizing you for writing polyglot code. It's not uncommon to see constructs such as this in beginner code:

int *array = (int *)malloc(length * sizeof (int));

This is bad for several reasons. Casting malloc's return value loses the benefits of C's weak typing and violates DRY as the type is being needlessly repeated. Even if one realizes that sizeof is a compile-time operator and thus sizeof *array is preferable, the type is still being repeated in the cast. C makes an explicit exception for implicit casts to and from void * because void pointers are the generic type. Not only that, but it's not idiomatic to use malloc in C++. malloc is a libc function. C++ has its own standard library. C++ has new/delete and constructs like std::vector which are explicitly designed for the purpose of dynamically allocating memory. You lose the ability to use these facilities if you attempt to have compatibility between both languages at once.

// C
#include <stdlib.h>
int *array = malloc(length * sizeof *array);
...
free(array);

// C++
int *array = new int[length];
...
delete[] array;

// C++
#include <vector>
std::vector<int> array;
...
array.push_back(value);
...
for(auto it = array.begin(); it != array.end(); ++it)
    *it;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment