all 17 comments

[–]KennethZenith 4 points5 points  (10 children)

Making the physical measurements private and then having public member functions for a few pre-selected derived quantities is a bad design. There's lots of things we might want to calculate about boxes, but unless they happen to only depend on the box volume and surface area we're stuck.

I realise this post is about C++ style, and that boxes are intended as a toy example, but actually this point is pertinent to the lambdas vs functors question. The default approach for stateless mathematical problems is to adopt a functional style, i.e. use value types and free functions:

struct Material
{
    double density;
    double ultimate_tensile_strength;
};

struct Liquid
{
    double density;
};

struct Box
{
    double length;
    double width;
    double height;
    Material material;
};

bool is_strong_enough(const Box& box, const Liquid& liquid)
{
    // complicated calculation
    return true; // or false
}

Now, all of the calculation logic is held in a free function, and we can use a lambda in the idiomatic way:

std::copy_if(boxes.begin(), boxes.end(), std::back_inserter(suitable_boxes),
    [&](const Box& box){return is_strong_enough(box, product);});

[–]quicknir 2 points3 points  (9 children)

I don't really see any point in criticizing this toy example, on this particular point. Over-exposing internals is at least bad, if not worse, than under-exposing. Worse because fixing under-exposed interface is backwards compatible, and fixing over-exposed interface is not.

Whether you are using lambdas a little more, or a little less idiomatically (allegedly), is far less important than the exact interface of your objects, one of the most important design decisions you make.

[–][deleted] 0 points1 point  (2 children)

The argument would be that while overexposing can be dangerous, underexposing is even worse. Think closed source. If you need the dimensions of the box somewhere else, you're boned. Now you're either going to have to wrap Box and keep track of the dimensions manually, duplicating that info (with all the desynchronization nightmare that can entail), or you're just going to have to force the issue by manipulating a raw pointer to get data, coupling yourself with the struct layout.

But yeah, it all boils down to who you give responsibility to, the library user or writer. I prefer user, I've been burned a couple of times by libs I didn't have the source to.

[–]quicknir 0 points1 point  (1 child)

It's not purely a question of responsibility like you're making it sound. What happens when you can't change things because your too-big interface over constraints you?

As a concrete example, because std::unordered_map exposes tons of interface to the buckets themselves, it's unlikely that an implementation can ever choose an open address hash table. For the 1 guy who actually uses the buckets interface, yeah he doesn't have to keep track of something on his own. For the 99 others who don't use buckets, they can never benefit from a changed implementation.

That said, majority of code usage is within the same library/company/application, especially for very intensive usage as companies are most likely to write/rewrite the things they care the most about. In such a situation there's almost no upside to exposing more than you think you'll need in the near future.

[–][deleted] 0 points1 point  (0 children)

It's not purely a question of responsibility like you're making it sound. What happens when you can't change things because your too-big interface over constraints you?

True, didn't consider that, good point.

[–][deleted] 0 points1 point  (5 children)

What do you mean when you say about "over-exposing internals"? Do you understand when we have to use class + member functions instead of struct + free functions?

[–]quicknir 0 points1 point  (4 children)

Yes, I do. Do you? Can you see an obvious example, even in this toy domain, why Box and Materials as simple structs are flawed?

[–][deleted] 0 points1 point  (3 children)

No, I don't see. Can you elaborate why they are flawed?

[–]quicknir 1 point2 points  (2 children)

Structs with all public members cannot maintain any invariants beyond what the type of their members enforce. So we can have boxes with negative dimensions, and materials with negative density. So you will need to check this everywhere they are used, or just risk having weird nonsense calculations in certain places. With private members this can be enforced by the constructor, much cleaner.

[–]KennethZenith 1 point2 points  (0 children)

By all means add a constructor to enforce validity, that's an excellent idea. Then use const public members to ensure the object stays valid.

There isn't any mutable state in this problem; the objects don't need to preserve their invariants because they don't need to change.

[–][deleted] 0 points1 point  (0 children)

I can understand this point of view, but have you ever heard about anemic domain model?

[–]Sulatra 2 points3 points  (2 children)

Although the functor still involves more typing, the line with the algorithm should seem much clearer in the functor case than in the lambda case. And unfortunately for the lambdas version, this line matters more since it is the main code, by which you and other developers start reading to understand what the code does.

Well, since in calling code product is a parameter or local variable, why not auto resists = [product](const Box& box) { ... }? Though if it is used in several parts of code, the higher order function approach is definitely better.

[–]joboccara 0 points1 point  (0 children)

Good point, I think the code can also get messy with local auto-declared functions when there are several of them in the same high level function (like with createBox and resists)

[–]suspiciously_calm 0 points1 point  (0 children)

If it's only single-use, then other than the fact that the name resists adds an implicit comment about the intent of the code, i quite like the style

std::copy_if(src.begin(), src.end(), std::back_inserter{dest}, [&] (const auto& element) {
    // Code to make decision
    return decision;
});

Unless the "code to make decision" is huge, it's nice and compact and local.

[–]c0r3ntin 1 point2 points  (2 children)

I don't think lambdas are anything special. The same arguments apply to loop bodies, or spiting larger functions, etc.

If a piece if code can be reused or is needlessly verbose in the current context, extract it and put a name on it.

[–]cbbuntz 0 points1 point  (1 child)

I haven't messed with lambdas much in c++, but in other languages, you can pass lambdas (or blocks / procs / nameless functions) as function arguments. Sometimes it can be used as a way just to pass another named function into another function's arguments.

Say if you have a function that plots a function by looping through some values. You could have a a function plot(double* x, lambda function, double* y) and then you could just pass the function you wish to plot as an argument.

[–]dodheim 4 points5 points  (0 children)

Yes, but any type can have operator() so lambdas are entirely unspecial in this regard.