Bjarne Stroustrup, an intelligent man

Have you ever seen this lecture?

Not only is the guy a top-notch computer language designer, he is intelligent full stop.

There is more than just computer talk in this lecture, lots of very relevant ideas of human organization and overcoming obstacles in general.

3 Likes

At 25:30 comes the reason why I am finding it hard to like the latest iterations of C++.

I don’t see C++ being a “better C” any more. Its a different language altogether.

If I am wrong, please explain why I am wrong. I am open to counterargument and would like to change my view in fact, in order to get over my distaste of how the C++ language looks now.

And I am fully willing to admit my distaste stems from me not understanding the reasons why such convoluted structures have been invented.

On this video, it is very clear that original C++ design was driven by the need to provide an efficient language for the purpose of driving real-world software development. Current C++ language looks to me like that never-ending academic quest for perfection that Stroustrup wanted to avoid.

Could someone please prove me wrong and give a practical example of how badly I have misunderstood the basis of current C++ language evolution? Who is driving it and to what purpose? Do the new additions to the syntax actually derive from real-world problem solving?

I realize I am about 15 years late in asking this question and will accept just a good link to where the subject is argued.

39:23 “I saw an interesting example recently, where the C++ code (of a physics problem) was immediately readable by someone who didn’t know C++ but knew the physics. I on the other hand (Bjarne Stroustrup) didn’t understand the function because although I knew the C++ I didn’t know the physics”

Whereas current C++ is completely illegible to someone who is not fluent in the C++ language syntax no matter even if they understand the concept of the underlying function

… but, maybe I will just have to bite the bullet and start reading cpprefrence

3 Likes

And, if this guy does not get you hot with full eye contact while drinking water from a tiny cup, then you sir, have a problem :laughing: :joy:

The_Design_of_C  _,_lecture_by_Bjarne_Stroustrup

6 Likes

I had the strange feeling I was the only one having this problem whenever new C++ features that diverge from old C/C++ are being used. Now I feel much better, thank you. :+1:

Not really sure it’s worth the pain.

Do you have a concrete example? The point of many of the changes was to make things easier to read.

I don’t recommend reading the reference in one go. Just skim the new features so you know that they’re there, then go back for the details when you actually need them.

2 Likes

https://aras-p.info/blog/2018/12/28/Modern-C-Lamentations/

Note that I am not putting C++ vs C here. I am saying that C++ is getting worse (EDIT I am overemphasizing this argument to make my “argument” more clear, I know already its not true)

EDIT: and one of the main drivers seems to be the quest for reusability - something that even Stroustrup admits was not the main focus in the beginning but somehow now has become the holy grail

But, as a professional, you are much better in judging whether reusability > legibility in the real world

I’ll sit here silent and see how the forum will go on fire :fire::fire::fire:.
This seems like a great prelude for a cruel fight :grin:

3 Likes

I get the impression your mind is already made up,
so I don’t think it’s likely to be worth me spending much effort trying to convince you otherwise.

I guarantee you that a decent chunk of the language committee actually consists of people who use C++ regularly for programming as well as academics, so many features are most likely prompted by real world usage.

(Edit: Oops, forgot to fill in the rest of the sentence. :P)

As @FManga hints at, many features in the more recent standards have actually been to simplify the syntax to make advanced features more accessible.
E.g.

  • constexpr made it possible to compute things at compile time without needing to understand template metaprogramming
  • range-based for loops made it easier to correctly iterate over arrays and container types
  • std::variant was introduced to reduce the amount of boilerplate needed to implement a tagged union
  • Concepts and requirements were introduced to make certain things that are possible with templates (primarily verifying that a type meets certain requirements) easier to write and more accessible. Specifically they can achieve a large chunk of what SFINAE can do without the extra mental gymnastics.

Pick a feature and I can most likely give an example use case or explain the rationale behind it.

Often a lot of advanced features won’t be used by the average programmer, they’ll be used by library writers who want to provide more powerful libraries to make user code simpler, and the average user will just use the library.

E.g. std::optional:

void possiblyPrint(std::optional<int> integer = std::nullopt)
{
  if(integer.has_value())
    std::cout << integer.value();
}

You wouldn’t know it from looking at that code, but the implementation of std::optional is actually reasonably complicated.
Conceptually it’s similar to having something like:

template<typename T>
class Optional
{
private:
	bool hasValue;
	T optionalValue;
	
	// ...
};

But there’s a lot of non-obvious reasons why it’s more complicated than that.

E.g. optionalValue's default constructor mustn’t be called because it might have unwanted side effects, optionalValue can’t be a pointer because the object it points to could go out of scope, and dynamic allocation would be underisable, certain constructors must be conditionally explicit (which until C++20 means using SFINAE and std::enable_if).

The problem with this example isn’t really C++, it’s that Niebler has decided to write the code in a very ‘clever’ way that doesn’t make much sense unless you also have the definition of Ranges to go with it, and even then probably needs a lot of effort to make sense of.

The | operator has been overloaded to act as a kind of pipe operator that combines functions that return ‘ranges’.
Several other languages have similar ways of handling this kind of thing (e.g. C#'s IEnumerable, Java’s Stream).

It’s all based on how functional languages use cons lists and lazy evaluation to build up a structure that can filter and transform sequences but delays the filtering and transforming until each object is actually requested.

I’m not 100% sure but I think those lambdas could be defined as regular functions.
Presumably there’s some benefit to defining them as lambdas, or maybe Niebler just wanted to show off the ‘constrained lambdas’ feature,
or perhaps he’s actually a functional programmer and feels more comfortable with lambda expressions.

I don’t know for definite if this would work because I haven’t looked much into Niebler’s ranges (or ‘Niebloids’ as they’ve become known) because I’m not likely to need them any time soon, but theoretically the following code should be roughly the same and hopefully a bit more understandable:

// A sample standard C++20 program that prints
// the first N Pythagorean triples.
#include <iostream>
#include <optional>
#include <ranges>   // New header!
  
// maybe_view defines a view over zero or one
// objects.
template<Semiregular T>
class maybe_view : view_interface<maybe_view<T>>
{
private:
	std::optional<T> data_;

public:
	maybe_view() : data_(std::nullopt)
	{
	}

	maybe_view(T t) : data_(std::move(t))
	{
	}

	const T * begin() const noexcept
	{
		return data_.has_value() ? &data_.value() : nullptr;
	}

	const T * end() const noexcept
	{
		return data_.has_value() ? &data_.value() + 1 : nullptr;
	}
};
 
// "for_each" creates a new view by applying a
// transformation to each element in an input
// range, and flattening the resulting range of
// ranges.
// (This uses one syntax for constrained lambdas
// in C++20.)
template
<
	std::Range R,
	std::Iterator I = std::iterator_t<R>,
	std::IndirectUnaryInvocable<I> Fun
>
constexpr auto for_each(R && r, Fun fun)
requires std::Range<std::indirect_result_t<Fun, I>>
{
	return std::forward<R>(r)
		| std::view::transform(std::move(fun))
		| std::view::join;
};
 
int main()
{
	// Define an infinite range of all the
	// Pythagorean triples:
	using view::iota;

	auto triples =
		for_each(iota(1), [](int z)
		{
			return for_each(
			iota(1, z + 1),
			[=](int x)
			{
				return for_each(
				iota(x, z + 1),
				[=](int y)
				{
					if ((x * x + y * y) == (z * z)) 
						return maybe_view(make_tuple(x, y, z)))
					else
						return maybe_view<T>();
				});
			});
		});
 
	// Display the first 10 triples
	for(auto triple : triples | view::take(10))
	{
		auto x = std::get<0>(triple);
		auto y = std::get<1>(triple);
		auto z = std::get<2>(triple);
	
		std::cout << '(';
		std::cout << x << ',';
		std::cout << y << ',';
		std::cout << z;
		std::cout << ')';
		std::cout << '\n';
	}
}

Note that iota (named after an operator from the relatively obscure programming language APL) is more or less equivalent to Python’s range(1, z + 1), C#'s Enumerable.Range(1, z + 1) or Haskell’s [1..z + 1]

The use of the overloaded | is one of those things where brevity has been chosen in favour of explicitness.
In C# the equivalent to the loop at the end would be something like:

foreach(var triple in triples.Take(10))
{
	var x = triple.Item1;
	var y = triple.Item2;
	var z = triple.Item3;
	
	Console.Write('(');
	Console.Write("{0},", x);
	Console.Write("{0},", y);
	Console.Write(z);
	Console.Write(')');
	Console.WriteLine();
}

Both languages actually have a kind of ‘tuple syntax’ that would make unbinding x, y and z clearer to people who know what the syntax means, but I’ve left that out.

Lastly, I’d like to note that the author mentions coroutines at the end.
Those are actually slated to be included in C++20,
which means, unless something changes, the example given by the author will become legal C++20 code:

generator<std::tuple<int,int,int>> pytriples()
{
    for (int z = 1; ; ++z)
        for (int x = 1; x <= z; ++x)
            for (int y = x; y <= z; ++y)
                if (x*x + y*y == z*z)
                    co_yield std::make_tuple(x, y, z);
}
1 Like

Thanks, but your worry is premature. I recognize my shortcomings and I am always ready to change my views given a good counterargument.

2 Likes

It’s hard to give counterexamples without knowing which parts you think are fine and which parts are ‘illegible’ and ‘convoluted’.

I could pluck features out of a hat,
but then it would be easy for people to claim that I’ve cherrypicked features.


In regards to who is driving it, the ISO C++ committee consists of both academics and ‘normal’ language users, many of whom are specifically chosen delegates from tech companies (both the big ones like Microsoft and IBM and the lesser-known ones like Edison Design Group and Vollman Engineering).

If you want to know more about the committee group,
there’s a general page here and a list of the WG21 members here.
(They’ve even got people who are involved in maintaining LLVM and Clang, not to mention Bjarne Stroustrup and Herb Sutter themselves.)

As for how it’s driven, there’s a page actually detailing the process a feature proposal goes through. Anyone can propose a feature by submitting a detailed formal paper, but the process for accepting a feature is quite rigorous.

(Edit: The committee is a bit like this forum really - loads of people of varying experience from different countries writing program code and grumbling at each other in English. :P)


As @FManga hinted at, cppreference is great for learning the standard library and the rules (with full technical terminology and legalese),
but it’s not intended to be used for actually learning the language.
It’s exactly what it says on the tin - a reference.
You refer back to it when you want to know some specifics without resorting to reading the standard.

For actually learning the language, https://www.learncpp.com/ is still probably the best tutorial since it actually incorporates newer features and demonstrates their usage.


When it comes down to it, no matter what you think of the syntax,
C++ fills a very particular spot in the language world that few other languages do.

Most high level languages have memory overhead due to being dynamically typed (e.g. Python, JavaScript, Lua), using lazy evaluation (e.g. Haskell), or they require GCs (e.g. C#, Java, Go, Haskell, Python, JavaScript, Lua), whilst most low level languages tend to be weakly typed and let you get away with murder without even hiding the evidence.

C++ is in the rare position of being strongly typed, statically typed, strictly evaluated, not using a GC and having both high level and low level constructs.

There are a few others that are comparable (e.g. Rust, Nim (if you disable the GC)),
but they’re still relatively few and they’re generally all quite young (in language terms - i.e. less than 10-15 years old) or have a small userbase.

(Note: Rust is my favourite candidate to become the ‘new’ C++ because of its ownership semantics.)


P.S. If you really want to know what it’s like to be lost in abstract nonsense, read some Haskell code.

1 Like

Keep in mind that Stroustrup also said the physics code was illegible to him. Different types of coder require different sets of tools. C++ always had a “deep end of the pool”. Did you ever look at the source code of map/vector/auto_ptr? That “complicated” code was written (pre-C++11) to solve problems in ways that are both easy to use and safer than the alternative (writing your own containers for specific types or using a container of void*). Despite it being reusable. Post-C++11 features are just more of the same.

Business logic and low-level code are two very different things, they should be written differently, and they should be kept separated (even in non-OOP code). Business logic should not know the concept of a thread, co-routine, socket, template, allocator, move semantics, or reflection.
With pre-C++11 you had these silly situations:

example(getThing()); // error
Type thing = getThing();
example(thing); // no error

So “complicated” new features (in this case, move semantics) allow libraries to be written that make business logic easier to read/write.

I think most of the panic on Twitter is because, when features for one type of code/coder are added, the other type looks at it and freaks out. When you consider each programmer working within his domain, no-one sacrifices legibility.

Another problem is that flawed tools are being replaced with better ones and many people just don’t want to relearn. Volatile, for example, was flawed. The meaning itself was vague and broken ever since they came up with it in C (you don’t need data to be volatile, you need operations on the data to be volatile). auto_ptr was flawed to the point of being practically useless, shared_ptr and unique_ptr solve that but using them effectively involves a learning curve and a change in mentality.

3 Likes

You just described my favourite cup of tea right there

So you’d rather write printf("%s%d", 12, "Hello"); than std::cout << 12 << "Hello";?
Or long * pointer = malloc(sizeof(int)); rather than long * pointer = new int();?

dynamic memory allocation?

shivers

1 Like

I still much prefer printf

1 Like

There’s three reasons the std::cout approach is objectively better:

  • It’s type-checked
    • My example there purposely uses the "%s" pattern with 12 and the "%d" pattern with "Hello" to demonstrate that a rather obvious bug isn’t caught at compile time (unless the compiler supports extensions that have actually be tailored to printf).
    • With the operator << approach, getting the arguments the opposite way around to what you intended won’t cause a segfault, it’ll just print the other way around.
  • It’s extensible
    • printf doesn’t have a means of adding new patterns, but providing a new operator << for doing output to std::ostreams is easy.
    • The same technique can be applied to any kind of printing library, even retroactively (e.g. it could be used with Pokitto's print functions)
  • It’s more efficient
    • printf has to actually parse the string you give to it at runtime, whereas the use of operator << relies on function overloading at compile time to pick the correct format functions, and the operator chaining has zero overhead (at least it should do with any decent modern-ish compiler)

Granted they didn’t have to use operator <<, it would have been possible to set the library up in such a way that you could use:

std::cout.print(12).print('A');

And with C++11 it would have been possible to do:

std::cout.print(12, 'A');

But print would have to be a template and the extension system would actually have to use a different function, e.g. std::ostream & print_extend(std::ostream &, UserType).


Keep away from it on an embedded system whenever possible,
but on desktop it’s perfectly viable as long as you don’t overuse it.

At any rate, the point of my trick question was that long * pointer = malloc(sizeof(int)); will compile in C and silently cause a bug.
In C++, both statements will result in compile-time errors due to type mismatches,
thus preventing a runtime bug.

That’s what strong static typing is all about - preventing bugs, saving the programmer headaches.

2 Likes

I don’t know about this. Catching silent bugs was always the best part of programming.

My reason for preferring printf is for formatting output.
Consider the following c++ code to print 42 as a 0-padded 8-digit hexadecimal value:

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    ios state(nullptr);

    cout << "The answer in decimal is: " << 42 << endl;

    state.copyfmt(cout); // save current formatting
    cout << "In hex: 0x" // now load up a bunch of formatting modifiers
        << hex
        << uppercase
        << setw(8)
        << setfill('0')
        << 42            // the actual value we wanted to print out
        << endl;
    cout.copyfmt(state); // restore previous formatting
}

Whereas using printf

#include <cstdio>

int main()
{
    printf("The answer in decimal is: %d\n", 42);
    printf("In hex: 0x%08X\n", 42);
}

IMHO the printf version is far easier to read and determine the intended output, whereas the c++ version requires first reading all the state modifiers and if 42 is passed as a variable it can easily blend in with the various state modifiers when skimming the code. Whereas the printf equivalent the %08X is easily read as print a value in hex with 8 digits padded with 0's using uppercase letters. I’m not sure about other compilers but gcc will parse the string and warn about type mismatches and missing/extra arguments making it easier to ensure you have the intended results. When I want an object to print itself I do something like:

printf("The value of someObject is ");
someObject.print();
printf("\n");

I remember when I was first learning c++ there was a lot of talk about not putting pointers in dynamic arrays. The difficulty I’ve had with this argument is it depends on what the pointer is used for. Personally if I do need to use a vector for storing an array of complex objects I’ll typically store them as pointers and use new and delete to allocate/free them, but the memory management part is always part of a class that has ownership of the objects with a destructor that will properly free them when done. The Qt versions of lists, vectors, ect. automatically store values as dynamically allocated pointers in the background for objects larger than the size of a pointer because it saves on memory access when the array needs to be expanded (especially useful for vectors) because it only needs to copy the array of pointers rather than the objects themselves.

The easiest thing I’ve found to managing memory with new/delete is to simply make sure they’re in the same area. If class A calls new to allocate objects on the heap then class A should also be in charge of calling delete to free said objects at some point in time. Very rarely, and only in special circumstances, is it necessary for a class/function to allocate an object on the heap and return a pointer to it but passing ownership of the object to the caller and making them responsible for freeing it.

For this community there’s really two main groups of c++ users:

Group 1: Working on learning c++ (be it new or just learning the newest features) and are making games using what they’ve learned as a way to test out their knowledge. This group should pay extra attention to the nuances of the language and utilize newer features like constexpr, lamdas and others to their fullest extent since that’s their goal.

Group 2: Just wants to have fun making games as a hobby without really wanting to master the languages. This group should concern themselves with ill-formatted code (ie. code that can cause unexpected behavior), but otherwise doesn’t really need to prefer static constexpr over #define if they’re not sure what the difference is (they should know exactly what their code is doing), and should often focus more on optimizing areas where needed (saving some space if needed, or improving frame rate), but otherwise stick to the “it works” mentality lest they get bogged down with all the different complexities c++ has to offer ultimately getting discouraged from community involvement because they “just want to have fun and make games”.

4 Likes

You could always write your programs in assembly,
then you’ll have lots of silent bugs to catch.

Better yet, use butterflies. :P


I don’t disagree, it is unfortunate,
and it’s not even the most awkward part of C++'s IO streams.
However, that’s a limitation of how they’ve chosen to implement the system rather than a limitation of the language.

It would be entirely possible to make something that isn’t stateful.

For example...
template<typename Int>
struct hex_format_t
{
	Int value;
};

template<typename Int>
hex_format_t<Int> format_hex(Int value)
{
	return { value };
}

// This would either need to be done in a generic way,
// or have a specific implementation for each integer type.
std::ostream & operator <<(std::ostream & stream, hex_format_t<std::uint8_t> & format)
{
	constexpr char digits[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', };
	
	stream << digits[(format.value >> 4) & 0xF] << digits[(format.value >> 0) & 0xF];
	return stream;
}

Which you’d then use as std::cout << format_hex(42) << '\n';.
(The idea would of course be to make this work for all integer types, not just std::uint8_t.)

That’s a very simple example, it would be possible to create something that can accomodate lots of different formats and even combine them.

And of course you can always create functions to hide the state changing if it’s something you’re going to be doing a lot.

For example...
template<typename Int>
struct format_08X_t
{
	Int value;
};

template<typename Int>
format_08X_t<Int> format_08X(Int value)
{
	return { value };
}

std::ostream & operator <<(std::ostream & stream, hex_format_t<std::uint8_t> & format)
{
	std::iot state(nullptr);
	state.copyfmt(stream);
	
	stream << "0x";
	stream << std::hex << std::uppercase << std::setw(8) << std::setfill('0');
	stream << format.value;
	
	stream.copyfmt(state);
	
	return stream;
}

Then you’d just std::cout << "In hex: " << format_08X(42) << '\n';

There’s probably at least half a dozen other tricks you could do to make the format saving code less obtrusive. (I can think of at least one easy one that would make the 'copyfmtstuff part of the<<` chain.)

It’s probably worth pointing out that in C++20 they’re planning to introduce std::format which is more along the lines of how C#'s string formatting works.
It still has some of the flaws of std::printf but I believe it’s fixed the extensibility part at least.

It will, but that’s a compiler extension, it’s not part of the language.
VC++ will also warn about it, but by rights it should be a hard error, not just a warning.

On the one hand if copying is expensive then resizing the array is expensive, yes.
(Strictly speaking, as of C++11 it’s almost certainly a move rather than a copy, so something like a vector of vectors would be as cheap as doing a pointer copy.)

On the other hand, arrays of pointers really upset the cache,
and on a modern CPU, a lot of the time the cache miss is more expensive than the copy.

That said, it’s not a dichotomy, there are other options.

About std::deque

One decent alternative is to use std::deque which doesn’t do any object copying when it needs more memory.
It’s typically implemented as a dynamic array (i.e. like a std::vector) of pointers to fixed-size arrays, like so:
deque sketch

That gets you the best of both worlds - it minimises both cache misses and copying on resizing.

Naturally there are trade offs, but that’s just the nature of programming.

(There’s an interestingset of benchmarks here. Naturally it can’t necessarily be generalised to all systems, but it’s useful nonetheless.)

I beg to differ.

auto charBuffer = std::make_unique<char[]>(1024);

That’s it. No deletion, the heap-allocated buffer deletes itself when the object falls out of scope and the generated code would be as if you’d written:

auto charBuffer = new char[1024];
// ...
delete charBuffer;

But you don’t need to worry about returning early without having deleted the buffer.
In fact, one that people often forget about: if a function called before delete throws an exception, the buffer doesn’t get deleted because delete is never reached.
With std::unique_ptr, even if an exception is thrown the buffer is still deallocated.

I.e. Leak:

auto charBuffer = new char[1024];
throwsAnException();
delete charBuffer;

No leak:

auto charBuffer = std::make_unique<char[]>(1024);
throwsAnException();

I know I’m perfectly capable of handling new and delete because I’ve written code that uses them extensively without leaking anything, but I still use the smart pointers for several reasons:

Reasons...
  • There’s no need to write a custom destructor or move assignment operator - the default implementations provided by the compiler will defer to the appropriate functions provided by the smart pointer, which will handle any reference counting and releasing without even having to think abou it
  • It’s harder to make a mistake - forgetting to delete is easy, but with smart pointers you don’t have to worry about it.
    • The only thing you have to be careful of is to avoid cyclic references and strong references when using std::shared_ptr, which is why std::weak_ptr exists - to break the cycles and provide a reference that won’t stop the memory being cleaned up.
  • For std::unique_ptr there’s practically no overheads, it’s just generating the code that you’d be writing yourself
Vectors of unique pointers...

Going back to the ‘vector of pointers’ case mentioned earlier,
if std::deque wasn’t suitable, a std::vector of std::unique_ptr would be a better bet than a std::vector of raw pointers because you wouldn’t have to worry about clean-up.

You’re still allowed access to the raw pointer through the use of .get() if you need to hold onto it briefly without taking full ownership of the pointer, so it should still be just as cheap, but you no longer need to worry about deallocating.

I disagree with this particular point, but possibly not in the way you’d expect.

I think everyone who is learning the language, regardless of what they want to do with it, should be taught to use constexpr and const variables for constants.

For Group 2, they probably shouldn’t even be taught about #define in the first place.

That’s a bold claim, but I stand by it.

As you say:

And constexpr variables are far easier to predict.

The value is fixed at compile time, so you’re not going to accidentally insert some code that does extra calculations at runtime.

For example:

#define FACT_4 factorial(4)

constexpr int fact_4 = factorial(4);

Whoever goes around flooding their program with FACT_4 is going to get a nasty surprise at runtime when they discover that FACT_4 isn’t actually a constant.

In my experience most of the people who turn their nose up at constexpr aren’t beginners,
they’re established programmers who think constexpr is just being used because it’s new and fancy.
constexpr is used because it’s predictable, obvious, safe, easy to use and solves a number of issues that macros have.


Also, since you mentioned static - be very careful with using static on global variables.
Generally it’s not what you actually want to be doing.

3 Likes