Felix' Ramblings
<< I Hate: Communication
>> I Hate: Python

2023.04.28
I Hate: C++ (and its whole toolchain)

I've touched (not properly used) a few programming languages over the years. But holy hell do I despise C++. Let's go over a few things which are awful, in no particular order of madness.

The C++ specification is not freely available [0]. You can get drafts of the specification for free (and hope for the best that no important changes are made to the final release). This is good enough in practice, but it's still bothering me out of principle. The latest specification (I could find) contains 2105 pages. Let that sink in. This is the reason why you can spend your entire life learning just this language.

This language contains an enormous amount of what feels like duplicate ways to express the same thing. The talk "The Last Thing D Needs" by Scott Meyers contains gems like the following:

Or casts: I'm not even going to pretend to know the differences between the following cast operations:

But funnily enough, the few times I needed to work with C++ these cast operations weren't even sufficient:

Or when iterating over something:

Trying to find out what the differences between these things is one hell of a ride: There are some rule of thumbs on which operator to use. More accurate explanations contain wonderful paragraphs like the following:

Confused what this means? Just read this concise explanation, that won't take any time at all [1].

Do you dislike the ambiguity of the static keyword in C? Boy do I have a treat for you. Again, not claiming my understanding of the semantics are 100% correct, but you get the point:


static int x; // variable is "private" to the compilation unit

static int testFunc() { // function is "private" to the compilation unit
  static int y;         // variable is persistent between function calls
}

class testClass {
  static int z;      // "class variable", shared across all class instances
  static int func(); // "class function", can be called without a class instance
}

C++ isn't even a superset of C. While the feature of designated initializers was implemented in C99, it took C++ almost 20 years to implement a weaker version in C++20. And compound literals just straight up not exist at all:


struct A { int x, y; };
struct B { struct A a; };
 
// Designated initializers
struct A a = {.y = 1, .x = 2}; // valid C, invalid C++ (out of order)
int arr[3] = {[1] = 5};        // valid C, invalid C++ (array)
struct B b = {.a.x = 0};       // valid C, invalid C++ (nested)
struct A a = {.x = 1, 2};      // valid C, invalid C++ (mixed)
// Note: out-of-order designated initialization, 
// nested designated initialization, 
// mixing of designated initializers and regular 
// initializers, and designated initialization 
// of arrays are all supported in the C programming 
// language, but are not allowed in C++. 

// Compound Literals
return (struct A){1, 2}; // Valid C, invalid C++

Then there's object oriented programming in conjunction with C++: I could probably fill a rant just about this particular paradigm, but I'll keep myself short. Creating a .cpp and a .h for each class is fucking tedious. Changing a function signature becomes busy work [2].

Don't even get me started with compiler messages. Not only does the template system bloat up the errors:


FAILED: src/tool-cli-utilities/CMakeFiles/tool-cli-utilities.dir/cli.cpp.o 
/bin/c++  -I./tool/src -I./tool/src/tool -I./tool/cmake-build-debug/include/resources/3rdparty/parallel_hashmap -I./tool/resources/3rdparty/cpphoafparser-0.99.2/include -I./tool/cmake-build-debug/include -isystem ./tool/resources/3rdparty/l3pp -isystem ./tool/resources/3rdparty/gmm-5.2/include -isystem ./tool/cmake-build-debug/include/resources/3rdparty/toolEigen -isystem ./tool/resources/3rdparty/exprtk -isystem ./tool/resources/3rdparty/modernjson/src -isystem ./tool/cmake-build-debug/resources/3rdparty/cudd-3.0.0/include -isystem ./tool/resources/3rdparty/sylvan/src -isystem ./tool/resources/3rdparty/cpptemplate -isystem ./tool/build/resources/3rdparty/carl_download/source_dir/src -isystem /usr/include/eigen3 -isystem ./tool/build/resources/3rdparty/carl/resources/include -g -fPIC -std=gnu++17 -MD -MT src/tool-cli-utilities/CMakeFiles/tool-cli-utilities.dir/cli.cpp.o -MF src/tool-cli-utilities/CMakeFiles/tool-cli-utilities.dir/cli.cpp.o.d -o src/tool-cli-utilities/CMakeFiles/tool-cli-utilities.dir/cli.cpp.o -c ./tool/src/tool-cli-utilities/cli.cpp
In file included from ./tool/src/tool-cli-utilities/model-handling.h:54,
                 from ./tool/src/tool-cli-utilities/cli.cpp:15:
./tool/src/tool/storage/SymbolicDecomposition.h: In instantiation of 'tool::dd::Bdd<LibraryType> symMec(const tool::dd::Bdd<LibraryType>&, uint_fast64_t, tool::dd::Bdd<LibraryType>*, tool::dd::Bdd<LibraryType>*, tool::dd::Bdd<LibraryType>*, tool::dd::Bdd<LibraryType>*, const std::set<tool::expressions::Variable>&, const std::set<tool::expressions::Variable>&, const std::vector<std::pair<tool::expressions::Variable, tool::expressions::Variable> >&) [with tool::dd::DdType Type = tool::dd::DdType::CUDD; ValueType = double; uint_fast64_t = long unsigned int]':
./tool/src/tool/storage/SymbolicDecomposition.h:526:37:   required from 'std::vector<tool::dd::Bdd<LibraryType> > symbolicMECDecomposition(const tool::dd::Bdd<LibraryType>&, const tool::dd::Bdd<LibraryType>&, const tool::dd::Bdd<LibraryType>&, const tool::dd::Bdd<LibraryType>&, const std::set<tool::expressions::Variable>&, const std::set<tool::expressions::Variable>&, const std::vector<std::pair<tool::expressions::Variable, tool::expressions::Variable> >&, uint_fast64_t) [with tool::dd::DdType Type = tool::dd::DdType::CUDD; ValueType = double; uint_fast64_t = long unsigned int]'
./tool/src/tool-cli-utilities/model-handling.h:784:103:   required from 'std::shared_ptr<tool::models::ModelBase> tool::cli::buildModel(const tool::cli::SymbolicInput&, const tool::settings::modules::IOSettings&, const tool::cli::ModelProcessingInformation&) [with tool::dd::DdType DdType = tool::dd::DdType::CUDD; ValueType = double]'
./tool/src/tool-cli-utilities/model-handling.h:1687:51:   required from 'std::shared_ptr<tool::models::ModelBase> tool::cli::buildPreprocessModelWithValueTypeAndDdlib(const tool::cli::SymbolicInput&, const tool::cli::ModelProcessingInformation&) [with tool::dd::DdType DdType = tool::dd::DdType::CUDD; BuildValueType = double; VerificationValueType = double]'
./tool/src/tool-cli-utilities/model-handling.h:1708:106:   required from 'std::shared_ptr<tool::models::ModelBase> tool::cli::buildPreprocessExportModelWithValueTypeAndDdlib(const tool::cli::SymbolicInput&, const tool::cli::ModelProcessingInformation&) [with tool::dd::DdType DdType = tool::dd::DdType::CUDD; BuildValueType = double; VerificationValueType = double]'
./tool/src/tool-cli-utilities/model-handling.h:1728:107:   required from 'void tool::cli::processInputWithValueTypeAndDdlib(const tool::cli::SymbolicInput&, const tool::cli::ModelProcessingInformation&) [with tool::dd::DdType DdType = tool::dd::DdType::CUDD; BuildValueType = double; VerificationValueType = double]'
./tool/src/tool-cli-utilities/model-handling.h:1745:75:   required from 'void tool::cli::processInputWithValueType(const tool::cli::SymbolicInput&, const tool::cli::ModelProcessingInformation&) [with ValueType = carl::RationalFunction<carl::FactorizedPolynomial<carl::MultivariatePolynomial<cln::cl_RA> >, true>]'
./tool/src/tool-cli-utilities/cli.cpp:272:82:   required from here
./tool/src/tool/storage/SymbolicDecomposition.h:416:61: error: no matching function for call to 'getSingleSymbolicSCC(tool::dd::Bdd<tool::dd::DdType::CUDD>&, tool::dd::Bdd<tool::dd::DdType::CUDD>, tool::dd::Bdd<tool::dd::DdType::CUDD>&, const std::set<tool::expressions::Variable>&, const std::set<tool::expressions::Variable>&)'
  416 |             tool::dd::Bdd<Type> sNew = getSingleSymbolicSCC(v, s && ( ! t), *transitions, metaVariablesRow, metaVariablesColumn);
      |                                         ~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

But the whole build process is fucked. People like to compile basically each individual .cpp file. Assuming your build process works (make, cmake, ninja, meson or whatever the fuck people now use), you can get helpful linker errors like the following:


[7/7] Linking CXX executable bin/tool
FAILED: bin/tool 
: && /bin/c++ -g -rdynamic   -rdynamic src/tool/CMakeFiles/tool-main.dir/tool.cpp.o -o bin/tool  -Wl,-rpath,./tool/cmake-build-debug/lib:./tool/build/resources/3rdparty/carl:./tool/build/resources/3rdparty/carl/resources/lib:  lib/libtool-cli-utilities.so  lib/libtool-counterexamples.so  lib/libtool-parsers.so  lib/libtool.so  /usr/lib/libboost_filesystem.so  /usr/lib/libboost_system.so  /usr/lib/libz3.so  /usr/lib/libglpk.so  resources/3rdparty/cudd-3.0.0/lib/libcudd.a  resources/3rdparty/sylvan/src/libsylvan.a  /usr/lib/libhwloc.so  ../build/resources/3rdparty/carl/libcarl.so.14.22  /usr/lib/libboost_filesystem.so  /usr/lib/libboost_system.so  /usr/lib/libboost_program_options.so  /usr/lib/libboost_regex.so  /usr/lib/libboost_unit_test_framework.so  -ldl  -lpthread  ../build/resources/3rdparty/carl/resources/lib/libginac.so  /usr/lib/libcln.so  /usr/lib/libgmpxx.so  /usr/lib/libgmp.so  -lpthread  lib/libtool-version-info.so && :
/bin/ld: lib/libtool.so: undefined reference to `std::vector<tool::dd::Bdd<(tool::dd::DdType)1>, std::allocator<tool::dd::Bdd<(tool::dd::DdType)1> > > tool::storage::symbolicDecomposition<(tool::dd::DdType)1, double>(tool::models::symbolic::Mdp<(tool::dd::DdType)1, double> const&, unsigned long)'
/bin/ld: lib/libtool.so: undefined reference to `std::vector<tool::dd::Bdd<(tool::dd::DdType)0>, std::allocator<tool::dd::Bdd<(tool::dd::DdType)0> > > tool::storage::symbolicDecomposition<(tool::dd::DdType)0, double>(tool::models::symbolic::Mdp<(tool::dd::DdType)0, double> const&, unsigned long)'
/bin/ld: lib/libtool.so: undefined reference to `std::vector<tool::dd::Bdd<(tool::dd::DdType)1>, std::allocator<tool::dd::Bdd<(tool::dd::DdType)1> > > tool::storage::symbolicDecomposition<(tool::dd::DdType)1, carl::RationalFunction<carl::FactorizedPolynomial<carl::MultivariatePolynomial<cln::cl_RA, carl::MonomialComparator<&carl::Monomial::compareGradedLexical, true>, carl::StdMultivariatePolynomialPolicies<carl::NoReasons, carl::NoAllocator> > >, true> >(tool::models::symbolic::Mdp<(tool::dd::DdType)1, carl::RationalFunction<carl::FactorizedPolynomial<carl::MultivariatePolynomial<cln::cl_RA, carl::MonomialComparator<&carl::Monomial::compareGradedLexical, true>, carl::StdMultivariatePolynomialPolicies<carl::NoReasons, carl::NoAllocator> > >, true> > const&, unsigned long)'
/bin/ld: lib/libtool.so: undefined reference to `std::vector<tool::dd::Bdd<(tool::dd::DdType)1>, std::allocator<tool::dd::Bdd<(tool::dd::DdType)1> > > tool::storage::symbolicDecomposition<(tool::dd::DdType)1, __gmp_expr<__mpq_struct [1], __mpq_struct [1]> >(tool::models::symbolic::Mdp<(tool::dd::DdType)1, __gmp_expr<__mpq_struct [1], __mpq_struct [1]> > const&, unsigned long)'
collect2: error: ld returned 1 exit status
ninja: build stopped: subcommand failed.
Now you get to binary search for the line(s) which fuck up your compilation :) But that is if the build system works in the first place [3]. Let me tell you the tale of when I tried to build a project whose main source code consisted of ~200k LoC. Building the project on a cluster (for later benchmarks) greeted me with this wonderful error:

After the internet helped me decipher that this means that the compilation process is taking too many resources, I was a bit puzzled. But yes, this indeed was the case:

How the fuck can the compilation take 64GB of RAM? Well, the culprit was the line make -j in the build script: You see, when every .cpp is compiled individually into into its own compilation unit, build processes tend to parallelize these compilations. The line make -j however does not limit the number of parallel jobs. That means all .cpp files were compiled and optimized at the same time (instead of processing e.g. n files at a time), which apparently was sufficient to grind the system to a halt. Fun times.

Bonus Entry (2023.05.20)

Thanks to reddit, here's a fun edge case coming in C++23 (according to this page) for custom == / != operator overloads: This stackoverflow post sums up this proposal:

"if we find an operator == and an operator != in the same scope with the same parameters, use the C++17 rules. Otherwise, use the C++20 rules."

Wild.


[0]: To be fair, C, the language I "like", suffers from the same issue. Madness.

[1]: I'm not hating on that guy btw, there is no way I'd be able to produce an explanation less than twice its size. It's the language that is fundamentally fucked.

[2]: C is no better in this regard, but C++ especially shoves it down your throat with templates.

[3]: I'm still using a simple .sh / .bat file using a single compilation unit.


<< I Hate: Communication
>> I Hate: Python
 Felix' Ramblings