Am 15.06.22 um 11:13 schrieb Johannes Meixner:
On 2022-06-15 02:51, Aaron Puchert wrote:
In my view this encourages bad habits: if you can't pin dependencies you have to read the documentation and rely on documented behavior only. What often happens (and is the reason for pinning) is that developers rely on undocumented behavior that breaks in future versions (could even break with recompilation) and perhaps doesn't even work reliably.
Yes, this is how it is "out there in the wild". And it cannot be avoided in practice. Perhaps it cannot be avoided even in theory when free software development is allowed.
Maybe I'm a bit spoiled, because the libraries that I work with are reasonably documented, and I will generally complain in reviews if something relies on undocumented behavior. We teach new hires early on to work with documentation and write their own. In any event, while all this can't be fully avoided, we usually build with GCC, Clang, Apple Clang, MSVC on three architectures and three operating systems (sadly no big endian anymore), and that catches quite a bit of "relying on undocumented behavior". Many packages on GitHub make full use of the available CI services and test on a wide variety on platforms and sometimes even against multiple dependency versions. Not coincidentally, such projects tend to have a good track record of producing high quality software.
To rely on documented behavior the behavior would have to be sufficiently documented but often the behavior is not sufficiently documented and it cannot be sufficiently documented because often function A in libA calls function B in libB that calls function C in libC so the behavior of function A depends on the behavior of function B and function C and the behavior of the kernel in its environment. > In theory the developer of a program that calls function A fully understands the behavior on all lower levels.
In practice this is not possible with reasonable effort. So the developer of a program just calls function A and tests how his program behaves in his environment(s) and that's basically all he can do with reasonable effort. This however is problematic whether we use containers or not. If you're just inferring behavior, you might be inferring it subtly wrong. Testing can prove your code wrong, but it can almost never prove it right. So
That is of course not feasible. You're absolutely right about those dependencies, and if you don't address this issue pinning will only provide temporary relief. Of course we'll have a hard time if those behaviors are moving targets, so the idea [1] is that functions specify contracts (preconditions, postconditions), and then implementations are verified against those contracts under the assumption that all called functions satisfy their contract. This localizes our reasoning to a single function, and if you do this for all functions, you've proven correctness of the entire program. Even though full formal verification doesn't seem practical right now (though see [2]), these ideas have "proven" quite powerful and every reasonably reliable piece of software that I've seen uses them in some way. (Sometimes contracts are "by convention", e.g. size() on a C++ container is generally assumed to return an integer value specifying the number of elements. Sometimes they aren't fully written down. But if you don't try to impose clear and concise contracts, you will soon end up with a mess because the complexity will overwhelm your reasoning powers.) As a side note, partial verification is becoming more common. Aside from Rust obviously, we are gaining some checks in C/C++ as well [3,4]. They all use annotations on functions that specify pre- and postconditions, then work exclusively within a single function. that you have tested some combination is not as meaningful as you might think. It might do what you've wrongfully inferred in some, maybe even most cases, but then in others it breaks down. Pinning doesn't address the underlying issue. The underlying issue is not variability in implementation, but unclear contracts or contract violations. These are bugs, and variability in implementation can help you find them. Its absence can't reliably mitigate them. (Not generally at least, and if you can prove that it mitigates them you can probably also just fix the bug...)
Related to that:
As an example try to either output "Hello World!" or reasonably handle all possible issues in your program i.e. let the system terminate your program is not allowed instead your program must cleanly exit on its own as far as possible, e.g. SIGKILL does what it does but it is possible to handle even SIGABRT in some way. Cf. https://codegolf.stackexchange.com/questions/116207/hello-world-that-handles...
You could do that, but why? If you can't meaningfully continue, SIGBART isn't the worst idea.
And now imagine how far such careful programming is actually implemented "out there in the wild".
Fair enough, malfunction testing is a bit tricky. But we've been reasonably successful with simply running our existing unit test suite with instrumented allocation functions that fail after 1, 2, 3, ... allocations and then repeat the test until it passes. This exercises basically all error handling paths that we have. It doesn't test for strong exception safety, but in our code basic exception safety is usually good enough. But I feel that's off-topic. That error handling is not well-tested is a problem whether we use containers or not, since most errors have little to do with specific dependencies but other circumstances of execution. Aaron [1] <https://en.wikipedia.org/wiki/Design_by_contract> [2] <https://media.ccc.de/v/34c3-9105-coming_soon_machine-checked_mathematical_proofs_in_everyday_software_and_hardware_development> [3] <https://clang.llvm.org/docs/ThreadSafetyAnalysis.html> [4] <https://clang.llvm.org/docs/AttributeReference.html#consumed-annotation-checking>