We need to forge forward, and support the next generation of programming languages.
In all seriousness, C and C++ are with us forever. Improving and moving these languages forward, and improving their safety with static and runtime analysis (and hardware support) has to be part of the plan moving forward.
Retrofitting also doesn't solve the philosophy of a language. There are simply aspects of C++ that are too vital to its identity to change. Is object oriented programming the final word on abstraction in programming? Probably not. This is a young field, and there are always better solutions lurking around the corner. When we buy into the notion that something should live forever, we rob ourselves of the opportunity to move forward, or to at least know with certainty whether something is truly the best.
Unless we want to repeat what has happened with COBOL, where the systems have lasted forever to the point that all of the COBOL programmers are dead or retired, we need to start evolving our philosophy to favor language replace-ability, or stop guaranteeing backwards compatibility. The latter is untenable to most businesses, while the former can be achieved through the use of small services, FFIs, RPC, and system modularity.
I don't claim that existing systems are easy to replace, but I do posit that they can be made replaceable given that certain practices are adopted, and the mindset of the developers is that they system should be easy to remove and replace.
I also wouldn't say that Go is invalid, Go is more than production ready; it's actually in production in critical infrastructure today at scale.
Moreover, COBOL story just reminds me that there are still many cases that code should be maintained forever, and there is no unicorns to solve all problems like a magic. RPC introduces performance and mental overhead. FFI is limited by performance/vague boundaries. Even in backend development, there are lots of companies switching back to monolithic application after trying microservices.
It's also unfair to dismiss RPC or FFI, which have come a considerably long distance, and in many cases add no noticeable overhead. Today's networks are now approaching 400G speeds, and Linux has added considerably more interfaces such as eBGP bytecode that rewards a cross language mindset with better performance.
The problem of RPC is latency & increased complexity, not throughput. It's still hard to achieve native performance when passing objects between FFI boundary to avoid a copy, especially when using stub generators.
I choose languages through a series of criteria. First, based on my principles, which limits me to languages were safety, efficiency, and expressiveness are highly valued. I find that most languages have some core principles listed on their website. Then I take my software's requirements, and find a language in my subset of choices that is a best fit. If I am on a team, then we have to come to a consensus on the teams shared principles, which I find is very useful outside of choosing a language.
I think that services can be sized sufficiently large to be replaceable and still encapsulate most critical code paths such that sharing memory should be a rare requirement. Facebook and google are both examples of companies operating at hyper scale based on RPC service architecture. For those times were it is a requirement, I can't argue that RPC is a good choice. C is a great lingua franca, and most languages support its ABI (at least the ones used for critical performance).
As for C++ interop, ObjC++ is the best I've seen; is there something better?
If you believe that C++ "sacrificed" for backwards compatibility (and I agree), well what was that sacrifice for anyways, if not better compatibility?
It is a much better interop story to use this type of FFI. As I've previously said, C++ compilers are usually not 100% compatible with their C counterparts, and will actually change the semantics of attributes and make different decisions about inlining. It is far better to use a C compiler to get machine code that matches the intended semantics, and call over a well formed ABI that to take the subset language approach.
Not to mention that using C libraries from C++ adds a different convention for memory management in the middle of what should be RAII code, which almost always leads to memory leaks. By using the Rust FFI, Rust will at least make sure you are treating unsafe code in a controlled manner.
Here's a real example. If you write C, you'll probably make a system call, and then check errno. You might also write to errno, e.g. save and restore.
errno is implemented differently on every platform, but a C program can include errno.h, and use it portably. C++ can do the same thing: include errno.h and you're done.
But Rust can't read the C header, so it must literally special-case every OS [1].
errno is just one thing, but there's a long tail: different platforms have different types and size and function names and signatures, especially the ancient system-y stuff.
"Rust is better than C++ at C interop" is an absurd take. The point of Rust is to displace this stuff, not be the best at integrating with it.
1: https://github.com/rust-lang/rust/blob/7cb3ee453b829a513749e...
> But Rust can't read the C header, so it must literally special-case every OS [1].
That's a bit of an odd point. You're saying that the C standard library implementation of errno is different on each platform, but abstracted in the standard library so that C programs can just import it and run a portable function.
...and then you complain that Rust does the same...
Tier 1 is C++ and ObjC, where you can use C headers nearly directly.
Tier 2 is where you have to create (and maintain) bindings, but you can map practically everything. Here is Rust, D, Nim, Go, etc. More work than tier 1 is necessary but no loss of performance (except maybe inlining and macros). There are distinctions within this class depending on how well macros etc can be mapped, but usually people map to the ABI instead of the API.
Tier 3 is where you cannot map directly and inefficient adapters must be made. This is for example Java, which does not have struct or size_t. Also nearly all scripting languages (Python, Ruby, Lua, etc).
Like in any other language with similar feature, it is still a pain to manage in large APIs.
In rust, you can do dynamic dispatch via trait objects over C structs. In C++, that wouldn't work, because dynamic dispatch uses inheritance and it needs to add a virtual method table pointer in the struct, meaning that you'd need to translate/copy the struct into a C+ one.
Also, rust has neat tricks. An Option type of a reference is represented as a single pointer where NULL means None. That means your plain old C struct can be interpeted as an Option type in rust without copying/translation.
class cpp_class {
c_struct *pimpl;
public:
cpp_class(c_struct *p) : pimpl(p) { }
virtual void whatever();
}
class derived_cpp_class : public cpp_class {
public:
derived_cpp_class(c_struct *p) : cpp_class(p) { }
virtual void whatever(); // override
}; // Just copy paste this
template<typename T>
class CContainer {
protected:
T *pimpl;
CContainer(T* raw_ptr): pimpl(raw_ptr) {}
public:
CContainer(const CContainer&) = delete;
CContainer& operator=(const CContainer&) = delete;
virtual const char* get_my_name() = 0;
virtual ~CContainer() {};
};
// User
class MyCHandler : public CContainer<c_struct>
{
public:
MyCHandler(int arg) : CContainer(my_c_new(arg)) {}
virtual const char* get_my_name() override {
static const char[] kMyName = "MyC";
return kMyName;
}
virtual ~MyCHandler() {
my_c_destroy(pimpl);
}
std::unique_ptr<MyCHandler> clone() {
return std::make_unique<MyCHandler>(my_c_clone(p_impl));
}
};
Moreover, the zero-copy magic from pointer to option in Rust is not something like what you think - Rust relies on compiler magic to eliminate option type's heap allocation, and if you convert it from a pointer, it will reside on the heap.I'm sure that you can construct the container object at each call site so that it behaves like rust (and it would just be allocated on the stack), but it would get a little ugly. In rust, it's very seamless and "rust-like" even though you are operating on a native C struct.
Similarly, the Option type magic is pretty darn useful. I'm sure you could do that in C++ as well, but again it would end up ugly because you'd need to invent a new COption type or something.
In the general case, the hardware requires it; that's how computers work. The C library you're working with that has handed you the structure has not reserved any space for you to put anything into the C structure for it to have embellishments like type-dispatched virtual functions. The memory addresses immediately above and below the structure are off-limits (if they exist at all).
If you control the allocation and disposal of the memory, then you can easily embed the structure in a larger object. When calling the C code, it just gets a pointer to that part of the object that is the C structure it knows about. Amazingly enough, C++ can do this quite easily. It can be done not just via composition, but also by inheritance:
class derived : public some_c_struct {
public:
virtual method();
};
Then it's just: extern "C" void c_function(some_c_struct *);
{
derived d; // define easily "on the stack"
c_function(&d); // pointer upcasts to base implicitly
}
All you see here is 1995 draft vintage C++.The idea that C++ doesn't work smoothly with C structures is bizarre; C++ contains a dialect of C which is highly compatible with C90 (to the extent that large programs can be developed such that they compile as C or C++).
In rust, it creates the trait object right as it's calling a function that requires one. But "creates" is too strong a word -- it's just sending the virtual function table pointer along with the structure pointer and any other arguments, probably in registers. Only if you are actually storing the trait object somewhere does it matter much.
In your example, you have to construct the "derived" type each time you come from C to C++. Using an extra type is annoying. I think a better C++ example would be trying to do something closer to what rust does and use the C struct pointer most places, and construct the C++ object right before you need it. Would work, but doesn't come out quite as clean as it does in rust.
In any case, the main point is that the normal mode of C++ is to add vtable pointers to the structures to achieve dynamic dispatch, and adding stuff to the beginning of a struct does not interoperate with C code very well. The normal mode of rust is to not touch the struct layout and achieve these features through other means. I'm sure there's a way to work against the grain in C++ and make it work, but if you ask me, rust gets it a little cleaner in this particular case. Or you could argue that working against the grain is the normal mode of C++ ;-).
1. In Rust, you can define a method on a trait object (a type that encloses a C struct), and then invoke it dynamically with the syntactic appearance that 'self' is the struct. C++ does not allow you to add virtual methods to C structs.
2. In Rust, given a C struct T, you can convert T* to an Option<&T> without copying.
This stuff is all gravy but I wonder about the meat. Is there a good story for consuming C headers directly? C structs are typically a hash of typedefs and preprocessor gunk; can Rust navigate that?
What does this mean? A struct is just a block of memory with zero type information. So how does Rust do dynamic dispatch on C structs again?
Which is perfectly doable in C++ too.
As managed languages dev, I seldom use C++, but when I do it is the best system language currently available regarding tooling, libraries and hardware support (NVidia now makes GPGPU optimized for C++ semantics).
For app development, there aren't many scenarios where C++ is really necessary, whereas for systems it is going to stay around for years to come.
So anything that helps improving the quality of GCC, LLVM, CUDA, UWP, Metal Shaders, DirectX, Unreal/Unity/CryEngine/Cocos/Godot is more than welcome.
For anything else, we can just follow Microsoft's security suggestions as presented at Blue Hat IL, namely C# (replace by favourite managed language), Rust and modern C++.
https://github.com/Microsoft/MSRC-Security-Research/blob/mas...
[1] https://blogs.nvidia.com/blog/2019/02/05/adacore-secure-auto...
They also support initiatives to have Java, .NET, Julia target their GPGPUs.
And they have supported Fortran since early days as well.
Which is why OpenCL by being so focused in plain old C, with manual compilation, never got that much love from researchers.
For cross platform work, C++ is hard to beat and no languages comes close when it comes to tooling. For example, I can debug from Objc into C++ using XCode or Java to C++ in Android Studio.