Less "on accident" and more "as an unavoidable consequence of". Take for example:wareya wrote:I don't really think C++ makes it easy to change the API version of a library on accident. I do think that there's a linker problem, but I think it's exactly the same one that C has, which it does, when glib updates and breaks old userspace programs.
Code: Select all
class Foo {
void do_init(int a);
int var1;
public:
template<typename T>
Foo(const T &arg)
{
do_init(std::hash(arg));
}
int get_var(); /* returns var1 */
int get_var_info(); /* returns info about var1 */
};
Code: Select all
virtual void do_init(int a);
Code: Select all
int var1;
int var2; /* helps track info about var1 */
public:
int get_var_info(); /* returns info about var1, using var2 for help */
If this part of the language was better designed, these breaks wouldn't need to happen. You wouldn't have to come up with workarounds like the pimpl paradigm, or pure virtual classes, both of which do have an effect on run-time performance. Conceptually, fixing this is rather easy. Since (non-virtual) class methods are nothing more than a normal function with an implicit 'this' pointer parameter, do something like this:
Code: Select all
class interface Foo {
template<typename T>
static Foo *Make(const T &arg);
int get_var();
int get_var_info();
};
Code: Select all
class Foo; /* Forward-declare Foo class. */
Foo *Foo__Make(const int &arg); /* for each type used with the call */
int Foo__get_var(Foo *this);
int Foo__get_var_info(Foo *this);
If you want to get clever, you can even work in inheritance to selectively expose details to the binary interface where it would be beneficial to do so. For example
Code: Select all
// Base class for intrusive pointer reference counting
class RefBase {
std::atomic<int> ref = 0;
public:
virtual ~RefBase() { }
int add_ref() { return ref.add_and_fetch(1); }
int dec_ref()
{
int ret = ref.sub_and_fetch(1);
if(!ret) delete this;
return ret;
}
};
class interface Foo : public RefBase {
/* Foo's layout includes and starts with RefBase, but anything more about it (more data fields, more virtuals, etc) is unknown. */
template<typename T>
static Foo *Make(const T &arg);
int get_var();
int get_var_info();
};
But maybe there's nuances about the language that makes this not possible. Which would be unfortunate since every time I try to write modular C++ code, I wish for something like it.
My interpretation of what he was talking about is a bit more general than that. Rather than an explicit "version", it's more about what each individual function or namespace/class provides and requires. If some lib updates to provide more or require less, you're good, just keep on keeping on. But if it would update to provide less or require more, that's a break. And instead of breaking the function/namespace/whatever, leave it alone and create a new one that sits next to it so it satisfies both old and new programs alike. This way if you link to, for example, SDL-20160425, you can load and run with SDL-20170226 just fine since everything that was there and the things it did is still there doing the same things. You can load and use SDL-20221010 too, though you can't use SDL-19790101. And if you're going to break in a way that old apps can't work with it, make a new library that won't be confused with the old one.The accretion he's referring to is to basically consider the API version part of the identity of the library. You wouldn't link "SDL2", you'd link "SDL2 2.0.4".
The problem with semantic versioning he was pointing out was that it doesn't provide enough context. If the patch number changes, you don't care because you'll still run. If the minor version changes, you don't care you'll still run. If the major version changes, you're screwed. Or really, you might be screwed, which is worse. Even if you successfully link with it, there's little to say what behavior changes there are; it might work, or it might not, or it might work sometimes. So out of the three numbers, you only care about one of them, and that one doesn't tell you all you'd want to know.No backwards compatible version changes like semver tries and fails to provide.
So rather than mess about with all that, simply don't break compatibility. "Avoid breakage by turning it into accretion." The entire purpose of that would be to maintain backwards compatibility.
I'd imagine because things like dynamic arrays and variants can be implemented using the language spec. Unless there's a clear advantage to being in the language spec, leaving them in the standard library reduces the amount of clutter in the language itself. If C is just A+B, then it would be redundant to specify A, B, and C. Just specify A and B, then define C's behavior in terms of A and B.Things like dynamic arrays and variants should by all means be part of the language spec. For some reason they're not.
Obviously nothing like this is so clear cut. I'm sure there are advantages to defining them as core language features. But it becomes of a question if the advantages outweigh the disadvantages.