IS C++ DOOMED?

I was bored so wrote a contiguous queue in C++ ( https://github.com/Tednesday/cpp-contiguous-circular-queue ). These are my thoughts from that exercise.

INTRO

I’ve written a lot of data structures before, but I’ve never written one that is “idiomatic”. After doing it, I’m left with the question, is it actually feasible to do any of this correctly?

Things like move semantics, hidden copy semantics, operator overloads, complex and implicit initialisation logic, exception safety etc etc, have resulted in a
combinatorial explosion of possible outcomes when it comes to the most trivial and minutae of operations, making writing basic data structures rather difficult. And the biggest problem is that you are forced to care about these things, whether you opted in or not, because everything ultimately effects everything else.

AN EXAMPLE

Simple questions often lead to complex problems.

Let’s say I want to know if a constructor failed. I have two options, one is to pass
in an in-out parameter, the other is to the throw an exception. The former is annoying
because now you have complicated initialisation due to the addition of a parameter. (lets not even touch different kinds of initialisation). So let’s throw an exception instead.

MyConstructor() {
  /* do stuff */
  if (something_bad) {
    throw exception;
  }
}

But exceptions come at a performance cost. So ideally we want to turn them off. So let’s turn them off and just accept we can’t really determine if a constructor failed or not.

Congrats you just opted out of lots of the functionality of the STL. Reluctantly you switch them back on. I want to use idomatic C++ right?

Now you have the issue that every single operation can throw an exception. That means you need to write code that wraps every possible resource in RAII logic. Even when it makes the program more complicated.

int my_function() {
  
  FILE file = open_file();
  do_stuff_a(file);
  do_stuff_b(file);
  c = d;

  close_file(file);
}

The above example is very simple. But it is not exception safe. do_stuff_a, do_stuff_b and c = d might throw an exception and so we would need to wrap our FILE so that in the event of an exception it will be correctly destroyed.

class FileWrapper {
  FILE file;
  FileWrapper() {
    file = open_file();
  }
  ~FileWrapper() {
    close_file(file);
  }
}

int my_function() {
  
  FileWrapper file_wrapper; 

  do_stuff_a(file_wrapper.file);
  do_stuff_b(file_wrapper.file);
  c = d;
}

Okay. Fine right? Not really. Instead of all the logic being contained within the scope of one function we now have an additional wrapper class with 2 extra functions. I dont think that’s easier to understand. (I’m aware that this is a somewhat trivial example)

This also introduces new challenges. Where do I jump to? What objects will get destroyed? How do I know what operations will throw? And now suddenly something that was supposed to be easy just became a lot more complicated.

Anyway the point is, you can’t do something simple, like check if a constructor failed, without using exceptions. And using exceptions means a full commitment to RAII for any custom data type. Idiomatic modern C++ isn’t a pick and mix, it’s a full course meal and you’ll eat it whether you like it or not.

Another big issue is copy and move semantics. Take something like return value optimisation (RVO).

It great for making function calls more ergonomic, but it messes with copy semantics. Now copies that result from calling a function have a different meaning to regular copies, even when they can look identical. And that’s assuming that RVO always works (which it doesn’t).

When we add in the many possibilites that can happen during a copy we suddenly need to spend a substantial time understanding exactly what is happening in some of the most simple operations of the program. Did it move? Did it copy? Was it elided? Does it need to be moved? Does it need to be copied? What else is that assignment doing? Why did it throw?

And when it comes to implementing move and copy constructors you are left feeling like you’ve definitely done something wrong. Just look at how many value categories there are https://en.cppreference.com/w/cpp/language/value_category

This is what friction in a design looks like. When two different parts of the design actively go against one another. When something that is supposed to do one thing does N number of things and the benefit is not obvious. Did we really need move semantics on top of value based copy semantics? Is copy elision actually the right choice? Is it a good thing that the initial meaning of what a copy was has now substantially changed?

THE COUNTER ARGUMENT

The argument is that all of this complexity is required. That is is useful. It allows us to solve problems more easily and reduce complexity.

In principal, I have nothing against that. But there are two issues.

The major issue is that the promise of the language is that “you do not pay for what you don’t use”. Picking and choosing features that best solve your current problem is supposed to be the whole selling point of C++. Unfortunately a lot of these features bleed into one another by design.

The second issue is that it is simply too complicated to write code that satisfy a lot of these requirements. And I really disagree with the argument that the complexity is required. It’s just not. It’s only required if you buy fully into idiomatic C++. The only reason it is as complicated as it is, is because every new features is trying to fix the mistakes of the last one, which is why there is substantial dependency between features. This doesn’t seem like a good thing to me. The complexity of the language is not helping reduce the complexity of my domain.

The solution?

There is a trend in modern programming language design that attempts to shift complexity so that it only exists behind an interface.

In C++ this manifests itself in the form of library implementers doing all the heavy lifting. Complexity is hidden behind interfaces and writing correct, idiomatic data structures is quickly becoming something that is not for mortal men.

This is a shame because writing custom data structures is very, very useful and we should be encouraging everyone to do it. But it is no longer a first class citizen in C++ because of uneeded and runaway complexity that is caused by years of adding things but never taking them away.

Try stepping into an STL implementation. Standard types should be understandable by the standard C++ programmer. It’s a failure of the language that the code is not easily understood.

I’m not particularly impressed by what the future holds. Many new proposals come off
as completely bizarre and detached from any observable reality. That is, of course, until you realise the design philosophy that is being followed. They literally just implement whatever the current fad in programming is.

Proposals seem more beholden to research funding, internet fame and ego rather than working towards some clearly defined design goals.

Professionally speaking these design decisions cost me money. Friction and complexity costs me money and my time. That’s why I’m not particularly amused
by design approaches that do not get me to my end goal.

I’m not in the business of writing the perfect program that satisfies an arbitrary standard. I’m in the business of making stuff with tools. The language is the means to the end, not the end itself and until C++ realises that, it is doomed.

2 thoughts on “IS C++ DOOMED?

Comments are closed.