Last active
March 9, 2026 16:52
-
-
Save johnmcfarlane/424fd6956a7a32b40ecedf95424eea53 to your computer and use it in GitHub Desktop.
How to handle C polymorphism in C++
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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