"Modern C++" auto and lambda

Started by
33 comments, last by nfries88 8 years, 1 month ago

So, to be clear, loops for control flow are sometimes unavoidable, and more often would become unnecessarily opaque -- that said, a lot of programming challenges that are solved with control flow loops can be made into transformation loops without confusion, and will often become composable as a result.

To whit:


I think an example of the type of modification I am talking about is in order. Let's say I have this loop:

for (float x : vector_of_floats) {
  std::cout << x << ' ';
}
std::cout << '\n';

So, you've got a bit of a bug (feature?) here with a trailing ' ' between the last value and the newline, but neither are good for composability in any case. I'd re-write this as follows:


auto it = vector_of_floats.begin();

if(it != vector_of_floats.end())
{
    std::cout << *it;

    for_each(++it, vector_of_floats.end(), [](float f) {
        std::cout << " " << f;
    });
}

The big thing here is that we aren't relying on indexing.


Now I want to format it differently, like this: "(4, 3, -1)". Now I need to do something different either for the first element or for the last one, so it's probably easiest to change the loop to indices.

std::cout << '(';
for (size_t i = 0; i < vector_of_floats.size(); ++i) {
  if (i > 0)
    std::cout << ", ";
  std::cout << vector_of_floats[i];
}
std::cout << ")\n";

Since my above is basically equivilent to this already, let me take the composability to its logical conclusion:


template <class I>
void print_separated_values(I it, I end, string separator)
{
    if(it == end)
        return;

    std::cout << *it;

    for_each(++it, end, [](float f) {
        std::cout << separator << f;
    });
}

// usage:
std::cout << "("; print_separated_values(vector_of_floats.begin(), vector_of_floats.end(), ", "); std::cout << ")";

If I then need to have a mask that indicates which indices are active at this time and I only need to print those, I would do

std::cout << '(';
bool some_element_printed = false;
for (size_t i = 0; i < vector_of_floats.size(); ++i) {
  if (!active[i])
    continue;
  if (some_element_printed)
    std::cout << ", ";
else
some_element_printed = true;
  std::cout << vector_of_floats[i];
}
std::cout << ")\n";

All I need in my solution at this point is a container which only contains active elements, or one which is partitioned with the desired elements up front. The algorithm std::stable_partition gives us that, with a slightly odd predicate -- since you're passing in the active array (I assume you mean that elements are boolean, and true if that index is to be included) we need to walk it within the predicate, so we init-capture a starting index which will be incremented each time the predicate is called, and the predicate returns what it finds at that index, selecting the active values from vector_of_floats so that they appear in order at the beginning of the vector.

Altogether that gives:


template <class I>
void print_separated_values(I it, I end, string separator)
{
    if(it == end)
        return;

    std::cout << *it;

    for_each(++it, end, [](float f) {
        std::cout << separator << f;
    });
}

// usage: (note: lambda init capture is a C++14 feature)
auto last = stable_partition(vector_of_floats.begin(), vector_of_floats.end(), [int i = 0](float /*ignore*/) { return active[i++]; });

std::cout << "("; print_separated_values(vector_of_floats.begin(), last, ", "); std::cout << ")";

Now, we didn't save any lines of code (and I've not run that through a compiler), but I argue that the code in my solution is better for not mixing all those responsibilities inside a single loop, and because we're left with a generalized function for printing a list of values separated by arbitrary characters.

It would take some time to attack your NegaMax, but I bet it can be done. As an aside, there aren't all that many algorithms in the standard library, but its also valid to implement your own in the same style (many of which can be implemented as combinations of those provided).

Sean Parent has given several great talks on this very subject, such as C++ Seasoning (youtube).

throw table_exception("(? ???)? ? ???");

Advertisement

According to Microsoft, using the auto keyword, as well as lambdas can make "cleaner, tighter and easier to read"(abbreviated of course) code. I am not in any industry, but merely a lowly hobbyist. So can someone explain to me the general reasons why this is whats being pushed by Microsoft and if it is being adopted by others, why I am wrong in my disagreement with these statements?

If you remove the standard c++ library, are lambdas not merely just a "fancy" way to scope a block of code? Are anonymous functions really THAT COOL?

And how is auto easier to read? You are implying and hiding the type unless you explicitly and physically look at what it is received by?

Not starting flame war, rather an educational opportunity for my self and others.

First of all, don't take anything said here as rules to follow. Most people find their own guidlines with these methods because it is really a standart of a company or a team rather than a standart to the whole world.

I find lambadas and auto really helpful in scoping types/behaviors that you know what they do.

If you write auto x = 3.0f; You'll need to comment wth is this x.

But if you write auto widthHeightRatio = 3.0f ; you know few things:

A) This is a ratio, therefore a dobule or a float.

B) The meaning of the variable, you don't use any magic here.

C) A variables exists since you declared it, and you don't care what it is. You know it's a ratio between height and width and you know it exists.

So this "auto" thingy lets you write more meaningful code. You don't care for types anymore, you care what about the variable's meaning. (And they are still strongly typed which is great).

Lambadas make easier usage for abstracting behavior, and simply a good tool to write less code.

There are very famous patterns which uses functions to provide a behavior. For example a generic Equal method.

Or if you are familiar with .net, using Linq. (Which again is a great way to show how it can help and grow C++ as a language).

lamdas are a feature C and C++ have needed for a long time. I can't say I love the syntax choices, but lamdas and the thread and atomic library additions really got me excited about C++11. I can't say I use them often - they rarely save any time or effort over just making a function, and I mostly work with older code anyway - but when you have a callback that literally just returns some value, or something like that, they're quicker than writing a whole function somewhere higher up in the file.

auto made me shudder from the beginning. One of the things I love about C and similar languages is that they require you to explicitly declare the data type of every variable. In a programming world full of incomplete (sometimes even erroneous) documentation, vague variable names, and misleading function names; last thing we need is more uncertainty.

Being familiar with your language's standard library

If you're writing an OS kernel, your language's standard library is unavailable.
If you're trying to make a reasonably complex program with a total size in the KB, you can't link to the bloated standard library. Most applications don't even use most of the standard library - IMO malloc, realloc, free, exit, abort, and assert, (s)printf, and some of the string functions are the only universally useful standard library functions. That's not to say I don't regularly use other standard library functions - the time and file functions in particular - but only in a small number of my applications. They'd serve me better as an "auxiliary" library. And then there's the worthless locale (usually, if you have to worry about localization, they are insufficient to fully accomplish the job) and string functions (unsafe). Honestly, when's the last time you've used div in production code?
Personally, I loathe the standard naming conventions too.

And don't get me started on the inefficient and incoherent mess that is STL.
Your argument is essentially that you can't be a C/C++ programmer without using the standard library. I'd argue that the best C/C++ programmers steer away from it more often than not.


And don't get me started on the inefficient and incoherent mess that is STL.
They are general purpose libraries. For general purposes they work well.

As general purpose libraries they generally work okay for general use, but they are rarely a perfect fit for most problems. They are good enough for most uses, but ideal for very few.

Years ago this was written about in Paul Pedriana's white paper on EASTL, EA's standard libraries. EA's libraries tried to respect the naming conventions and choices of the C++ standard library, but many classes had subtle changes. The changes made them less of a general purpose library with some additional usage complexity, but better for games by offering better performance or better debugging or better hardware support through alignment tweaks, etc.

There have been several other libraries similar to EASTL released from various groups over the years, but within our industry EA's libraries have touched such a large percentage of developers it is one of the best known.

While I agree that most businesses and game development shops tend to use their own libraries with better specialized support than the C++ standard libraries, that does not diminish what the standard libraries are. The standard libraries are there for everybody, they are taught enough that near-everyone knows them, and work well enough that they are near-universally understood. If you are using a more specialized library, you ought to be in a position where you can take the descriptions that rely on the standard library and interpret it in terms of your specialized library.

If you're writing an OS kernel, your language's standard library is unavailable.
If you're trying to make a reasonably complex program with a total size in the KB, you can't link to the bloated standard library.


Of course if you're doing something special then a specialized approach might be better. And if that's you, you probably know it. But that's not most people; even in non-AAA games you probably ought to start with the standard library (or some other well-known library) if its available on your platform unless and until experience shows you that you'll need to rip it out for something more tailored.

If those libraries are not available for whatever reason, you should still be implementing the algorithms yourself and building on them, not sewing raw loops hither and yon. This advice really is not about "use the standard library" per se (though it should be your first, best starting point unless you have good reason why it can't be) this advice is about not proliferating one-off loops.

Again, I advise anyone who thinks they disagree with this premise to watch some of Sean Parent's presentations that are available on YouTube. He demonstrates several real-world examples in which he takes 3 slides worth of hideous loops and reduces it to a mere handful of algorithms calls -- not only is the code shorter, he also eliminates tons of temporary state and conditionals (read: Sources of bugs), and makes it possible to reason about that the code does so that it can be evolved. And he reduces the big-O complexity to boot.

Honestly, your loops are not a special snowflake. If they are not themselves a known algorithm or combination of known algorithms, then they must be novel -- no one invents novel algorithms at the rate with which they throw down loops. Novel algorithms go in ACM papers, earn you PHDs, secure patents, and win you prestigious awards. They can make your career.

The honest truth is that 95% of programmers are ignorant of their algorithms such that they can't spot them in their own code. They don't write raw loops because they're faster, smarter, or better -- they write raw loops because haven't developed the mental faculties to think about those kinds of problems any other way. And they're worse off for it.

throw table_exception("(? ???)? ? ???");


...you should still be implementing the algorithms yourself and building on them, not sewing raw loops hither and yon.

This I can agree with. Especially since some algorithms are fairly complex, it makes it easier for new team members who shouldn't have reason to mess with it anyway to know what's actually happening there. And, on the original topic, the algorithms part of the library (count_if, find_if, remove_if, replace_if, etc) is one of the reasons why lamdas were so sorely needed in the standard, the ability of compilers to inline the use of the lamda can make use of the algorithm's performance roughly match that of writing your own loop.


This topic is closed to new replies.

Advertisement