Skip to content

Instantly share code, notes, and snippets.

@johnmcfarlane
Last active March 9, 2026 16:52
Show Gist options
  • Select an option

  • Save johnmcfarlane/424fd6956a7a32b40ecedf95424eea53 to your computer and use it in GitHub Desktop.

Select an option

Save johnmcfarlane/424fd6956a7a32b40ecedf95424eea53 to your computer and use it in GitHub Desktop.
How to handle C polymorphism in C++
// How to handle C polymorphism in C++: A Working Demonstration
//
// This document introduces C-style polymorphism, explains the main the safety
// issue it raises, and offers a technique to contain it using C++.
//
// Sometimes, we want to write a function that operates on an object without
// knowing its type. This is called polymorphism. A good example is sorting.
// We can sort a sequence of any type of object, so long as we know its order.
//
// The C standard library provides a quick-sort function, `qsort_r` (AKA
// `qsort_s`): https://en.cppreference.com/w/c/algorithm/qsort
// It takes a range of objects and a comparison function used to order them.
// You can use it to sort any types of objects because it is polymorphic.
//
// Let's use it to sort some rectangles:
#include <stdlib.h>
struct rectangle {
int width;
int height;
};
auto compare_width(void const* pointer1, void const* pointer2, void*) {
// step 2: pointers are cast from `void const*`
auto rectangle1{static_cast<rectangle const*>(pointer1)};
auto rectangle2{static_cast<rectangle const*>(pointer2)};
if (rectangle1->width < rectangle2->width) {
return -1;
}
if (rectangle1->width > rectangle2->width) {
return 1;
}
return 0;
}
void narrow_to_wide(rectangle* rectangles, int count) {
// step 1: pointer is cast to `void*`
qsort_r(rectangles, count, sizeof(rectangle), compare_width, nullptr);
}
// In the above code there are two levels of functions calls:
//
// 1. Our code calls `qsort_r`, which casts away the type of the objects
// 2. `qsort_r` calls our callback, `compare_width` which casts objects back to
// the correct type.
//
// By using a pointer-to-void, `qsort_r` is able to receive any type of object
// for sorting - as well as our comparison function. But this is risky: what if
// the types in these two casts don't match up? That's a serious bug!
// The first thing we can do is use a lambda expression to keep the casts
// together in a single function:
void short_to_tall(rectangle* rectangles, int count) {
auto const compare_width{
[](void const* pointer1, void const* pointer2, void*) {
// step 2: pointers are cast from `void const*`
auto rectangle1{static_cast<rectangle const*>(pointer1)};
auto rectangle2{static_cast<rectangle const*>(pointer2)};
if (rectangle1->height < rectangle2->height) {
return -1;
}
if (rectangle1->height > rectangle2->height) {
return 1;
}
return 0;
}};
// step 1: pointer is cast to `void*`
qsort_r(rectangles, count, sizeof(rectangle), compare_width, nullptr);
}
// This code does exactly the same thing (except we're ordering by width).
// However, it's easier to see that the casts match up. They're only three
// lines away. A reviewer only has to look at one function to confirm that all
// is well.
// But this function is quite limited. We need to write a new - mostly
// identical - function for every type for every order. Can we write a generic
// solution that takes a comparison function? Yes!
// With this solution, we could rewrite `short_to_tall` and `narrow_to_wide`
// without adding any new void casts to the program. And then we could use it
// to sort rectangles any other way, or to sort entirely different types of
// object:
template <typename T, typename Comp>
void qsort_typesafe(T* objects, int count, Comp compare) {
auto const compare_width{
[](void const* pointer1, void const* pointer2, void* context) {
auto const object1{static_cast<T const*>(pointer1)};
auto const object2{static_cast<T const*>(pointer2)};
auto const compare{static_cast<Comp const*>(context)};
return (*compare)(object1, object2);
}};
qsort_r(objects, count, sizeof(T), compare_width, &compare);
}
void small_to_big(rectangle* rectangles, int count) {
qsort_typesafe(
rectangles, count,
[](rectangle const* rectangle1, rectangle const* rectangle2) {
auto const area1{rectangle1->height * rectangle1->width};
auto const area2{rectangle2->height * rectangle2->width};
if (area1 < area2) {
return -1;
}
if (area1 > area2) {
return 1;
}
return 0;
});
}
// Note, there are many other ways to improve how we sort, e.g. `std::sort`
// (https://en.cppreference.com/w/cpp/algorithm/sort.html).
// But there are many other C libraries out there that use `void` casts to
// accept objects of arbitrary type. For example: sqlite, libarchive, and zlib.
// This approach can help reduce the number and proximity of those casts.
// Here's my working out:
#include <gtest/gtest.h>
#include <array>
#include <cstring>
auto operator==(rectangle const& lhs, rectangle const& rhs) {
return lhs.width == rhs.width && lhs.height == rhs.height;
}
TEST(rectangle, narrow_to_wide) {
std::array<rectangle, 3> actual{{{2, 5}, {6, 1}, {3, 4}}};
std::array<rectangle, 3> const expected{{{2, 5}, {3, 4}, {6, 1}}};
narrow_to_wide(actual.data(), actual.size());
ASSERT_EQ(expected, actual);
}
TEST(rectangle, short_to_tall) {
std::array<rectangle, 3> actual{{{2, 5}, {6, 1}, {3, 4}}};
std::array<rectangle, 3> const expected{{{6, 1}, {3, 4}, {2, 5}}};
short_to_tall(actual.data(), actual.size());
ASSERT_EQ(expected, actual);
}
TEST(rectangle, small_to_big) {
std::array<rectangle, 3> actual{{{2, 5}, {6, 1}, {3, 4}}};
std::array<rectangle, 3> const expected{{{6, 1}, {2, 5}, {3, 4}}};
small_to_big(actual.data(), actual.size());
ASSERT_EQ(expected, actual);
}
TEST(qsort_typesafe, reverse_lexicographical) {
std::array<char const*, 4> actual{{"the", "quick", "brown", "fox"}};
std::array<char const*, 4> const expected{{"the", "quick", "fox", "brown"}};
qsort_typesafe(actual.data(), actual.size(),
[](auto const* const* former, auto const* const* latter) {
return -std::strcmp(*former, *latter);
});
ASSERT_EQ(expected, actual);
}
int main(int argc, char** argv) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment