Is it bad practice to use a C++ compiler just for function
overloading?
IMHO standpoint, yes, and I'll need to become schizophrenic to answer this one since I love both languages but it has nothing to do with efficiency, but more like safety and idiomatic use of languages.
C Side
From a C standpoint, I find it so wasteful to make your code require C++ just to use function overloading. Unless you are utilizing it for static polymorphism with C++ templates, it's such trivial syntactical sugar gained in exchange for switching to an entirely different language. Further if you ever want to export your functions to a dylib (may or may not be a practical concern), you can no longer do so very practically for widespread consumption with all the name-mangled symbols.
C++ Side
From a C++ standpoint, you shouldn't be using C++ like C with function overloading. This is not stylistic dogmatism but one related to practical use of everyday C++.
Your normal kind of C code is only reasonably sane and "safe" to write if you're working against the C type system which forbids things like copy ctors in structs
. Once you're working in C++'s much richer type system, daily functions which are of enormous value like memset
and memcpy
don't become functions you should lean on all the time. Instead, they're functions you generally want to avoid like the plague, since with C++ types, you shouldn't be treating them like raw bits and bytes to be copied and shuffled around and freed. Even if your code only uses things like memset
on primitives and POD UDTs at the moment, the moment anyone adds a ctor to any UDT you use (including just adding a member which requires one, like std::unique_ptr
member) against such functions or a virtual function or anything of that sort, it renders all of your normal C-style coding susceptible to undefined behavior. Take it from Herb Sutter himself:
memcpy
and memcmp
violate the type system. Using memcpy
to copy
objects is like making money using a photocopier. Using memcmp
to
compare objects is like comparing leopards by counting their spots.
The tools and methods might appear to do the job, but they are too
coarse to do it acceptably. C++ objects are all about information hiding (arguably the most
profitable principle in software engineering; see Item 11): Objects
hide data (see Item 41) and devise precise abstractions for copying
that data through constructors and assignment operators (see Items 52
through 55). Bulldozing over all that with memcpy
is a serious
violation of information hiding, and often leads to memory and
resource leaks (at best), crashes (worse), or undefined behavior
(worst) -- C++ Coding Standards.
So many C developers would disagree with this and rightly so, since the philosophy only applies if you are writing code in C++. You most likely are writing very problematic code if you use functions like memcpy
all time in code that builds as C++, but it's perfectly fine if you do it in C. The two languages are very different in this regard because of the differences in the type system. It's very tempting to look at the subset of features these two have in common and believe one can be used like the other, especially on the C++ side, but C+ code (or C-- code) is generally far more problematic than both C and C++ code.
Likewise you shouldn't be using, say, malloc
in a C-style context (which implies no EH) if it can call any C++ functions directly which can throw, since then you have an implicit exit point in your function as a result of the exception which you can't effectively catch writing C-style code, prior to being able to free
that memory. So whenever you have a file that builds as C++ with a .cpp
extension or whatever and it does all these types of things like malloc
, memcpy
, memset
, qsort
, etc, then it is asking for problems further down the line if not already unless it is the implementation detail of a class that only works with primitive types, at which point it still needs to do exception-handling to be exception-safe. If you're writing C++ code you instead want to generally rely on RAII and use things like vector
, unique_ptr
, shared_ptr
, etc, and avoid all normal C-style coding when possible.
The reason you can play with razor blades in C and x-ray data types and play with their bits and bytes without being prone to causing collateral damage in a team (though you can still really hurt yourself either way) is not because of what C types can do, but because of what they'll never be able to do. The moment you extend C's type system to include C++ features like ctors, dtors, and vtables, along with exception-handling, all idiomatic C code would be rendered far, far more dangerous than it currently is, and you will see a new kind of philosophy and mindset evolving which will encourage a completely different style of coding, as you see in C++, which now considers even using a raw pointer malpractice for a class that manages memory as opposed to, say, a RAII-conforming resource like unique_ptr
. That mindset didn't evolve out of an absolute sense of safety. It evolved out of what C++ specifically needs to be safe against features like exception-handling given what it merely allows through its type system.
Exception-Safety
Again, the moment you are in C++ land, people are going to expect your code to be exception-safe. People might maintain your code in the future, given that it's already written and compiles in C++, and simply use std::vector, dynamic_cast, unique_ptr, shared_ptr
, etc. in code called either directly or indirectly by your code, believing it to be innocuous since your code is already "supposedly" C++ code. At that point we have to face the chance that things will throw, and then when you take perfectly fine and lovely C code like this:
int some_func(int n, ...)
{
int* x = calloc(n, sizeof(int));
if (x)
{
f(n, x); // some function which, now being a C++ function, may
// throw today or in the future.
...
free(x);
return success;
}
return fail;
}
... it's now broken. It needs to be rewritten to be exception-safe:
int some_func(int n, ...)
{
int* x = calloc(n, sizeof(int));
if (x)
{
try
{
f(n, x); // some function which, now being a C++ function, may
// throw today or in the future (maybe someone used
// std::vector inside of it).
}
catch (...)
{
free(x);
throw;
}
...
free(x);
return success;
}
return fail;
}
Gross! Which is why most C++ developers would demand this instead:
void some_func(int n, ...)
{
vector<int> x(n);
f(x); // some function which, now being a C++ function, may throw today
// or in the future.
}
The above is RAII-conforming exception-safe code of the kind that C++ developers would generally approve since the function won't leak no which line of code triggers an implicit exit as a result of a throw
.
Choose a Language
You should either embrace C++'s type system and philosophy with RAII, exception-safety, templates, OOP, etc. or embrace C which largely revolves around raw bits and bytes. You shouldn't form an unholy marriage between these two languages, and instead separate them into distinct languages to be treated very differently instead of blurring them together.
These languages want to marry you. You generally gotta pick one instead of dating and fooling around with both. Or you can be a polygamist like me and marry both, but you have to completely switch your thinking when spending time with one over the other and keep them well-separated from each other so that they don't fight each other.
Binary Size
Just out of curiosity I tried taking my free list implementation and benchmark just now and porting it to C++ since I got really curious about this:
[...] don't know what it would look like for C because I haven't been using
the C compiler.
... and wanted to know if the binary size would inflate at all just building as C++. It required me to sprinkle explicit casts all over the place which was fugly (one reason I like actually writing low-level things like allocators and data structures in C better) but only took a minute.
This was just comparing an MSVC 64-bit release build for a simple console app and with code that didn't use any C++ features, not even operator overloading -- just the difference between building it with C and using, say, <cstdlib>
instead of <stdlib.h>
and things like that, but I was surprised to find it made zero difference to the binary size!
The binary was 9,728
bytes when it was built in C, and likewise 9,278
bytes when compiled as C++ code. I actually didn't expect that. I thought things like EH would at least add a little bit there (thought it would at least be like a hundred bytes different), though probably it was able to figure that there was no need to add EH-related instructions since I'm just using the C standard library and nothing throws. I thought something would add a little bit to the binary size either way, like RTTI. Anyway, it was kinda cool to see that. Of course I don't think you should generalize from this one result, but it at least impressed me a bit. It also made no impact on the benchmarks, and naturally so, since I imagine the identical resulting binary size also meant identical resulting machine instructions.
That said, who cares about binary size with the safety and engineering issues mentioned above? So again, pick a language and embrace its philosophy instead of trying to bastardize it; that's what I recommend.
//
comments. If it works, why not?//
comments have been in the C standard since C99.