Post

C++ developers don't know how to be productive

Preface

I find that Western culture these days is obsessed with having the sender in communication explain every little detail. For example, boot.dev’s latest blog post has a big preface as well. This is because if the receiver disagrees with something and that has not been spelled out in detail, the sender usually gets an ad hominem response. I think that’s an absolutely dumb evolution of our communication style. Whenever you hear someone’s opinion, I expect the receiver of that opinion to put in effort to try and understand it. For those Westerners among us that require me to spoonfeed you, I am talking about Models of Communication and I expect you to put effort into understanding through reading up on the underlying topics and ask questions. For those more Easternly inclined, thank you for bringing some sanity into this world.

My Gripes With C++ Culture

C++ is used in a lot of different sectors. I personally have the most experience in the defense and automative industries, where the culture is more conservative. It is rare, for example, to find C++17, let alone C++20 there, as most places still use C for MCU development or similar low-level programming.

However, complexity is growing, and with it the team sizes as well as the demand for software maintainability.

Most C++ developers I have met in my life are adapt at the language, but strongly prefer their own development style. C++ being so widely used and its age means it has accumulated a lot of different paradigms. This is simultaneously one of its bigger strengths as well as its bigger weaknesses. Developers tend to work in a field where only a subset of C++ is useful. E.g. for very low-level work, the heap must be avoided. The templated allocator then can be employed to continue using dynamically allocating containers like vector, but without actually allocating on the heap. The Embedded Template Library (ETL) has some amazing resources for bounding sizes and preventing heap allocations, among other features.

As much as some libraries have to offer, developers want libraries to get out of their way as much as possible. Developer Experience (DevEx) is always important, but my personal experience in the C++ world is that autonomy and freedom are unduly valued too much.

Take for example Rust’s Tokio. That framework provides amazing tools for Rust developers to use async I/O, do multithreaded programming with more ease and provides a base for microservices. In essence, it’s a Microservice Bootstrapping Framework. The cost is, like much in Rust, that it forces the developer into certain patterns. Want to schedule a task every 5 seconds? No problem, just use interval. Want to multiple tasks that communicate one way or another? Well, hold on, you either use a Channel or you Arc<Mutex<>> all the shared data. What’s that, you are using the single threaded tokio executor? Yeah, nah, the compiler cannot verify that, so you’re stuck with those two options, or find another library.

I personally like this approach. Either use these two known, verified and working approaches, or use something else. But for a C++ developer, being forced to work in a certain way is not done.

Rust, being a relatively new/young language, has had the opportunity to create the right patterns from almost straight away. The first response I’ve received so far has mostly been “But why would I write all that boilerplate if I could just use a mutex?”. I’m pretty sure that the developer in that situation has then created their own Dunning-Kruger’d mind palace of an approach to C++, where the only right answer is one where they can continue developing as they always have: by reinventing the wheel.

I’m sure that other older programming languages probably suffer from the same, until something like a big change happens.

Currently Visible Cracks

And it is this conservatism / autonomy that is holding C++ back. The current cracks are cppfront and carbon. Because the current dominant culture is to conserve backwards compatibility against all costs, we’re stuck with a myriad of problems:

There are many, many things we’d like to change about C++. But it’s not only apparent in the language syntax/STL itself. C++20 has introduced coroutines as a completely new paradigm (another one, yay!). This enables a programmer to do all the fancy async/await stuff that other languages have had for years now. But of course, people only want to consume this in the aforementioned “I want to program myself into a ball of mud”-style.

For example, the C++ coroutine library cppcoro offers a way to run coroutines on event loops:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cppcoro::task<std::uint64_t> count_lines(cppcoro::io_service& ioService, fs::path path) {
  // do work
}

cppcoro::task<> run(cppcoro::io_service& ioService) {
  cppcoro::io_work_scope ioScope(ioService);

  auto lineCount = co_await count_lines(ioService, fs::path{"foo.txt"});

  std::cout << "foo.txt has " << lineCount << " lines." << std::endl;;
}

cppcoro::task<> process_events(cppcoro::io_service& ioService) {
  ioService.process_events();
  co_return;
}

int main() {
  cppcoro::io_service ioService;

  cppcoro::sync_wait(cppcoro::when_all_ready(run(ioService), process_events(ioService)));

  return 0;
}

At first glance, this looks great. You have the control of deciding when and where coroutines are run.

Except, notice that the io_service type needs to be carried into each function. Boilerplate and code coupling suddenly rear their ugly heads. Moreover, what happens when you turn this into a multithreaded program? You instantly get data races for shared state, unless you remember to wrap everything with Arc<M … I mean, std::mutex.

Frameworks and why I created Ichor

So obviously, I thought, this can be improved. Using the proper abstractions, coupling can be reduced. Forcing the code to only run on the same thread where the abstractions have instantiated them eliminates a large amount of multithreaded issues. An example would be sending a HTTP request in Ichor:

1
2
3
4
5
6
7
8
9
10
11
12
Task<std::optional<PingMsg>> sendRequest(std::string path, std::vector<uint8_t> &&toSendMsg) {
    std::vector<HttpHeader> headers{HttpHeader{"Content-Type", "application/json"}};
    auto response = co_await _connectionService->sendAsync(HttpMethod::post, path, std::move(headers), std::move(toSendMsg));

    if(response.status == HttpStatus::ok) {
        auto msg = _serializer->deserialize(std::move(response.body));
        co_return msg;
    } else {
        ICHOR_LOG_ERROR(_logger, "Received status {}", (int)response.status);
        co_return std::optional<PingMsg>{};
    }
}

Ichor executes this code under the hood by using an event loop (much like the cppcoro::io_service, except configurable). There is no carrying of parameters, nor is there any data race here. If a developer using Ichor creates multiple threads, they are forced to use the Channel abstraction or force state to be separate per thread.

Obviously, C++ does not have the checks that Rust has. Therefore, if the developer were to create threads in their own code, actively trying to circumvent Ichor, then data races can still occur.

Yet, as can be seen from the latest Reddit version update post, there are not many people who see the power of the framework. The first comment is again, “X provides something similar, why use Ichor?”.

Of course, this can be because of multiple reasons: I may suck at marketing/communication, I may have solved a ‘problem’ that no one has (though I doubt that) or there are other libraries that provide similar value.

But really, I think this is a combination of the embedded culture in C++, where people abhor being told how to write code. And Ichor certainly prescribes how to split up your code, what compiler flags you should use and asks you to invest into a complete framework.

Looking at Tokio, I have to admit that I am envious of what they have achieved. I certainly hope that people will come to see the value Ichor and C++ epochs bring.

This post is licensed under CC BY 4.0 by the author.