8 ms·
Disingenuous. The bad C++ code is in the very first line of the "make_appender" definition: capturing the closure's environment by reference is nonsense: It is
by ncmncm 4y ago
Disingenuous.
The bad C++ code is in the very first line of the "make_appender" definition: capturing the closure's environment by reference is nonsense: It is equivalent to returning a reference to an argument. It is not, then, a closure at all.
Using a correctly-defined make_appender would not, then, produce undefined behavior when you use it, with or without "move".
What the author has done here was to take a too-obviously wrong operation, returning a reference to an argument, but dress it up with syntax that will look less familiar to some readers, and pass it off as insightful.
But using a wrong function and getting wrong results is not surprising.
Piling on more uses after, that give more wrong results, does not reveal anything more.
When you need disingenuous arguments to make your point, it tells us more about your point than about the thing you are trying to make a point about. And, publishing anyway tells us more about you.
Returning that fake closure should evoke a compiler warning, if you turn on warnings.
- deleted 4y ago[deleted]
- benreesman 4y agoI was trying to be a little more diplomatic in my sibling reply because I've locked horns with the Rust community before and not enjoyed it, but you're not wrong.
- roca 4y agoA function returning a value that depends on the lifetime of the function's parameter is not crazy at all. Every class getter method that returns a reference to a member of the class does this.
- benreesman 4y agoSure. But returning the address of a stack-allocated object is (usually) broken. There isn't a right and wrong here: sometimes you want to opt in to the check for that, sometimes you want to opt out of it. Sometimes I actually do want to fuck with addresses on the stack in weird, potentially architecture-dependent ways, it's rare but it happens. I happen to think that Rust's linear/affine typing is by far the most usable low/zero-cost memory management model that anyone has demonstrated at scale and a real achievement in practical computer science, but it comes at a pretty serious cost in `Box`-this and `Arc`-that and `Rc`-other-thing and generally the borrow-checker being a PITA about some stuff we're used to doing. Rust is very cool and I use it, but the "using C/C++ is fucking strangers without protection"-vibe got old years ago.
- tialaramex 4y ago> returning the address of a stack-allocated object is (usually) broken. What matters is the lifetime, where the object lives is a rule of thumb for guessing lifetime that results from C++ trauma.
- ncmncm 4y agoHint: getters are code smell. Getters that return references are code stink.
- dataflow 4y agoYou can add [[clang::lifetimebound]] on the suffix parameter and you get <source>:21:35: warning: temporary whose address is used as value of local variable 'append34' will be destroyed at the end of the full-expression [-Wdangling] auto append34 = make_appender({3, 4});
- pfultz2 4y agoYou dont need annotations, cppcheck already warns with: test.cpp:16:45: error: Using object that is a temporary. [danglingTemporaryLifetime] assert((std::vector<int>{1, 2, 3, 4} == append34({1, 2}))); // FAIL: UB ^ test.cpp:3:12: note: Return lambda. return [&](std::vector<int>&& items) { ^ test.cpp:2:50: note: Passed to reference. auto make_appender(std::vector<int> const& suffix) { ^ test.cpp:4:36: note: Lambda captures variable by reference here. return append(move(items), suffix); ^ test.cpp:15:35: note: Passed to 'make_appender'. auto append34 = make_appender({3, 4}); ^ test.cpp:15:35: note: Temporary created here. auto append34 = make_appender({3, 4}); ^ test.cpp:16:45: note: Using object that is a temporary. assert((std::vector<int>{1, 2, 3, 4} == append34({1, 2}))); // FAIL: UB
- dataflow 4y agoAs awesome as that is, cppcheck doesn't seem ready for real-world use. Literally the first invocation I ran resulted in this error: error: Syntax Error: AST broken, binary operator '!=' doesn't have two operands. [internalAstError] explicit operator bool() const { return this->get() != pointer(); } This is for a simple wrapper class that looks like this: template<class T> class Foo : private Bar<T> { public: typedef value_type *pointer; pointer get() const { return ...; } explicit operator bool() const { return this->get() != pointer(); } }; Another example: void foo(uintptr_t const (&input)[2]) { if constexpr (sizeof(uintptr_t) == sizeof(int) && sizeof(long long) == 2 * sizeof(int)) { long long value; memcpy(&reinterpret_cast<uintptr_t *>(&value)[0], &input[0], sizeof(input[0])); memcpy(&reinterpret_cast<uintptr_t *>(&value)[1], &input[1], sizeof(input[1])); } } error: The address of local variable 'value' is accessed at non-zero index. [objectIndex] memcpy(&reinterpret_cast<uintptr_t *>(&value)[1], &input[1], sizeof(input[1])); ^
- dannymi 4y ago>The bad C++ code is in the very first line of the "make_appender" definition: capturing the closure's environment by reference is nonsense: It is equivalent to returning a reference to an argument. If it is so bad, it should (in the sense of how things would be in an ideal world) not compile. >It is not, then, a closure at all. It is a closure because all variables are closed-over and there are no free variables in the lambda body anymore. That is the definition of closure. >But using a wrong function and getting wrong results is not surprising. In an ideal world, there should be a compilation error. (There is in Rust) The majority of what's wrong in C++ is that it lets you do nonsensical (even dangerous) things, most of the time without even a warning (and not because it's technically impossible to warn--it just didn't occur to them). It's okay to acknowledge that--it's a product of its time. >Returning that fake closure should evoke a compiler warning, if you turn on warnings. That "should" tells me all I need to know. In the end either safety is important, or it isn't. Choose accordingly.
- jstimpfle 4y ago>> In the end either safety is either important, or it isn't. Choose accordingly. Not to disagree with your post but this comes across as very black-and-white.
- dannymi 4y agoTo expand: There's a prioritized list of requirements in engineering. Use the right tool for the job. Rust has: (see https://hackmd.io/@rust-ctcft/r1plN4You#/5 https://hackmd.io/@rust-ctcft/r1plN4You#/5 ) 1. Safety If you don't need that at the first slot, then the biggest strength of Rust doesn't apply to your problem (and you would waste time having to track lifetime parameters more than necessary to solve your problem).
- estebank 4y agoThere's also another item in that list, and that is performance. As soon as you slightly relax that one, Rust does become massively easier. A well placed .clone() or Arc makes the rest of the code performant enough and easier to understand, which makes the equation of choosing Rust over other alternatives in spaces that aren't necessarily systems programming less problematic.
- jstimpfle 4y agostd::vector<int> suffix{3, 4}; auto append34 = make_appender(suffix); // Version 1: test will work auto append34 = make_appender({3, 4}); // Version 2: test will fail I was in a mood to type the examples but I accidentally made Version 1 because I had started with the author's first example. I wondered where the problem was and couldn't find any (neither running nor reading the code) until I noticed the author had changed to Version 2. The problem is "obvious" in hindsight but one of the problems with C++ is that it makes certain things too implicit/convenient. I get that references are a staple feature in C++ but it's not at all obvious from the calling location that Version 2 is a bug. To see that, you have to navigate to the callee (finding which might be hard enough alone without a very solid IDE), and make sure that it's not capturing the argument by reference or not doing anything unsafe there. It's often a problem when a seemingly "value" argument is turned into actually a pointer-to argument at the callee. There are other languages that have this too, and I've never liked it. As someone who doesn't regularly code in C++ but has a solid understanding of the basics, I wonder why C++ ever allowed to have a reference parameter be called with a temporary? To me it feels like "References have value syntax but pointer semantics BUT you should program like it had value semantics"? Which to me would be exactly a premature optimization that is looking for trouble.
- benreesman 4y agoAgain, this isn't a right/wrong thing. Rust moves by default (and a lot of people find "=" a weird pun for that), C++ copies by default and has rvalue-references and an explicit `std::move`. If you want copy in your C++ lambda, you start it `[=](...) {...}`, if you want take a pointer in your C++ lambda, you start it `[&](...) {...}`, if you want something trickier you do trickier stuff. Rust opts you in to the nitpicky static analyzer and you have to opt out with `unsafe`, in C++ you have to opt in with e.g. `clang-tidy` or some annotations. They are remarkably similar, just with different defaults.
- tialaramex 4y ago> They are remarkably similar, just with different defaults. I'm going to lead with this, because I think it's most important: Culturally there's a world of difference. Safety is a part of Rust's culture. "Culture eats strategy for breakfast". Take sorting. In C++ the default sort is unstable, while in Rust the default sort is stable, that's just those defaults you mentioned (each has both kinds), although the choice speaks to culture. But look closer, in C++ the sort has undefined behaviour if your type isn't totally ordered. In Rust you can't sort the partially ordered types without saying how to order them fully. Still, in both languages we can write a custom order, so what happens then? In C++ if your custom order is nonsense you get... undefined behaviour. In Rust sorting won't necessarily work with a nonsense custom order but the behaviour remains well defined. > Rust opts you in to the nitpicky static analyzer and you have to opt out with `unsafe` Unsafe gives you a small number of dangerous "super powers" needed to write efficient low-level code, it does not opt out of the borrow checker's analysis, or indeed most other checks. This misconception makes me wonder how much of what you've written is conjecture rather than practical experience.
- CrendKing 4y agoThe fact that you may know that make_appender definition being bad does not mean every C++ user knows as well. It's also impossible for you to know ALL the possible bad, UB leading C++ code out there. I think the point the author tries to make is that, while C++ and Rust are probably "the same" for the most skillful and disciplined programmers (such as you), for average human, Rust just catches way more errors they make for them early. An extreme analogy would be trying to climb Everest all by yourself vs with a professional guide.
- benreesman 4y agoNot to disagree necessarily, but if you (give me a little rope) bucket languages that are hard to get past the compiler but more often correct when you do (Rust, Haskell), and languages where it's pretty easy to get something past the compiler and tweak it until it works well enough for your purposes (JS, C/C++), the tweak-it-until-it-kinda-works languages are fucking killing it on adoption.
- grumpyprole 4y ago> the tweak-it-until-it-kinda-works languages are fucking killing it on adoption. Industry clearly prioritises speed of delivery above all else. Security, reliability and maintenance are future problems (that would be nice to have). However, in order to gain the power to reason about our code and be able to prove the correctness of various properties (what a typechecker does), we have to program with more restrictive models. This has been argued many times before (e.g. Dijkstra's structured programming). Haskell and Rust are just two of many examples. My favourite is regular expressions, choose actual proper regexes and you have guaranteed O(n) execution, choose Perl/Python "Regex" and you have a potential security hole.
- zozbot234 4y ago> it's pretty easy to get something past the compiler and tweak it until it works That's just as true of Rust if you use clone(), Rc<> and RefCell<>. You just have to familiarize with a few boilerplate patterns, and the best part is you're only trading off a modicum of performance while preserving safety. But Rust can work quite well as a language for exploratory programming.
- crabbygrabby 4y agoIt's possible the author actually made this mistake, and because c++ is C++ they didn't realize why/how. Not a lot of programmers have a good handle on c++, maybe even most don't... Hence these other languages.
- ncmncm 4y agoIf the author deliberately chose to compile with warnings turned off, in order to present an example that would crash, then that tells us more about the author than about the point.
- deleted 4y ago[deleted]