1
\$\begingroup\$

I am quite new at C++ and I really wanna learn how to work with allocating memory. So I have created some utility functions. I am aware that C++ uses new and delete instead of malloc and free

Do these functions cause any memory leakage? (They probably do since I am experiencing some bugs).

// Includes.
#include<stdio.h> // for alloc() and del().

// Allocate new memory.
template <class T>
T* memalloc(slong_t alloc_size) { //}, const char* path = __builtin_FILE(), int line = __builtin_LINE(), const char* func = __builtin_FUNCTION()) {
    T* ptr;
    // print("Allocated ", alloc_size, ": called from <Func>", func, "[", path, ":",line ,"]");
    unsigned long n = (unsigned long) alloc_size;
    ptr = (T* ) malloc((n + 1) * sizeof(T));
    // ptr = new T[(unsigned long) alloc_size + 1];
    return ptr;
}

// Check if memory is allocated.
// Will cause segmentation fault if x is unallocated [const T* x;], when x is [const T* x = ""] it will run.
template <class T>
bool memnull(const T* x) {
    return x == NULL || ! *x;
}

// Delete allocated memory.
// Does not check if the memory is allocated.
template <class T>
void memdelete(T*& source) { //}, const char* path = __builtin_FILE(), int line = __builtin_LINE(), const char* func = __builtin_FUNCTION()) {
    // if (source != NULL) {
    // print("Deleting address", (address_t) source, "[", source, "]", ": called from <Func>", func, "[", path, ":",line ,"]");
    free(source);
    source = NULL;
    // } else {
    //  print("NOT Deleting", (address_t) source, "[", source, "]", "from", line, func);
    // }
}

// Memory equals.
template <class T>
bool memequals(const T* str1, const T* str2, slong_t size) {
    if (size == 0) { return true; }
    while (size-- > 0) { if (*str1++ != *str2++) { return false; }}
    return true;
}

// Move memory.
// Produces segfault when "dest = source".
template <class T>
void memmove(T* dest, const T* source, slong_t size) {
    if (size == 0) { return; }
    const T* s = source;
    if (dest < s) { while (size--) { *dest++ = *s++; }}
    else {
        const T* lasts = s + (size-1);
        T* lastd = dest + (size-1);
        while (size--) { *lastd-- = *lasts--; }
    }
}

// Copy memory.
template <class T>
void memcopy(T* dest, const T* source, slong_t size) {
    if (size == 0) { return; }
    const T* s = source;
    while (size--) { *dest++ = *s++; }
}

// Fill memory.
template <class T>
void memfill(T*& dest, T val, slong_t size) {
    if (size == 0) { return; }
    while (size-- > 0) { *dest++ = val; }
}

// Duplicate memory.
template <class T>
T* memduplc(T* source, slong_t size, slong_t new_alloc_size = 0) {
    if (new_alloc_size == 0) { new_alloc_size = size; }
    T* dest = memalloc<T>(new_alloc_size);
    if (source != NULL) { memcopy(dest, source, size); }
    return dest;
}

// Swap allocated memory.
// Only for move constructors.
template <class T>
void memswap(T*& dest, T*& source) {
    dest = source;
    source = NULL;
}

// Cast from const memory.
template <class T>
T* memcast(const T* source, slong_t size, slong_t new_alloc_size = 0) {
    if (new_alloc_size == 0) { new_alloc_size = size; }
    T* dest = memalloc<T>(new_alloc_size);
    if (source != NULL) { memcopy(dest, source, size); }
    return dest;
}

// Reallocate memory with a new size.
// Copies the content of "source".
template <class T>
void memrealloc(T*& source, slong_t size, slong_t new_alloc_size) {
    T* dest = memalloc<T>(new_alloc_size);
    if (source != NULL) {
        memmove(dest, source, size);
        source = NULL;
    }
    source = dest;
}

// Reverse memory.
template <class T>
T* memreverse(T* x, slong_t size, slong_t alloc_size = 0) {
    if (alloc_size == 0) { alloc_size = size; }
    T* arr = memalloc<T>(alloc_size);
    if (size == 0) { return arr; }
    slong_t li = 0;
    for (double ri = size - 1; ri >= 0; --ri) { arr[li] = x[(slong_t)ri]; ++li; }
    return arr;
}
template <class T>
const T* memreverse(const T* x, slong_t size, slong_t alloc_size = 0) {
    if (alloc_size == 0) { alloc_size = size; }
    T* arr = memalloc<T>(alloc_size);
    if (size == 0) { return arr; }
    slong_t li = 0;
    for (double ri = size - 1; ri >= 0; --ri) { arr[li] = x[(slong_t)ri]; ++li; }
    const T* carr = arr;
    // if (size > 0 and arr) { memdelete(arr); }
    return carr;
}

// Find the index of an item in an array.
// Does support negative index.
// Reverts "start" to "0" and "end" to "size" when the values are out of range.
template <class T>
slong_t memfind(const T* arr, T to_find, slong_t size, index_t start = 0, index_t end = 0) {
    start = start.subscript(size);
    if (start == npos) { start = 0; }
    end = end.subscript(size);
    if (end == npos) { end = size; }
    for (slong_t i = start; i < end; ++i) {
        if (arr[i] == to_find) { return i; }
    }
    return npos;
}

// Check if an array contains an item.
// Does support negative index.
// Reverts "start" to "0" and "end" to "size" when the values are out of range.
template <class T>
bool memcontains(const T* arr, T to_find, slong_t size, index_t start = 0, index_t end = 0) {
    return memfind(arr, to_find, size, start, end) != npos;
}

// Slice memory.
// Does support negative index.
// Reverts "start" to "0" and "end" to "size" when the values are out of range.
template <class T>
slong_t memslice(T*& x, slong_t size, index_t start, index_t end = 0) {
    start = start.subscript(size);
    if (start == npos) { start = 0; }
    end = end.subscript(size);
    if (end == npos) { end = size; }
    size = end - start;
    T* sliced = memalloc<T>(size);
    memmove(sliced, x + start, size);
    x = sliced;
    return size;
    //
    // slong_t index = 0;
    // for (slong_t i = start; i < end; ++i) {
    //  sliced[index] = x[i];
    //  ++index;
    // }
    // x = sliced;
    // sliced = NULL;
    // return index;

    // start = start.subscript(size);
    // if (start == npos) { start = 0; }
    // end = end.subscript(size);
    // if (end == npos) { end = size; }
    // size = end - start;
    // T* sliced = memalloc<T>(size);
    // for (slong_t i = start; i < end; ++i) {
    //  sliced[i] = x[(i - start)];
    // }
    // // if (!memnull(x)) { memdelete(x); }
    // x = sliced;
    // return (slong_t) size;
}

// Replace memory.
// Does support negative index.
// Resets "start" to "0" and "end" to "size" when the values are out of range.
template <class T>
void memreplace(T* source, T from, T to, slong_t size, index_t start = 0, index_t end = 0) {
    if (from == to) { return ; }
    start = start.subscript(size);
    if (start == npos) { start = 0; }
    end = end.subscript(size);
    if (end == npos) { end = size; }
    for (slong_t i = start; i < end; ++i) {
        if (source[i] == from) { source[i] = to; }
    }
}

// Shift error.
class ShiftError {
    public: const char* message;
    ShiftError(const char* x) { message = x; } };

// Fast memory shift.
// Resizes the memory with the specified "alloc_size".
// Parameter "end" must be "size + nshift".
// Produces segfaults when the values of "start" / "end" are out of range.
// Produces segfault when "nshift" is larger then "size" or smaller then "-size"
template <class T>
slong_t fastmemshift(T* source, int nshift, slong_t start, slong_t end, slong_t alloc_size) {
    T* shifted = memalloc<T>(alloc_size);

    // Shift to the right from start.
    if (nshift > 0) {
        memcopy(shifted, source, end);
        for (slong_t i = end - 1 + nshift; i > start + (nshift - 1); i--) {
            shifted[i] = shifted[i - nshift];
        }
    }

    // Shift to the left from start.
    else if (nshift < 0) {
        memcopy(shifted, source, start);
        for (slong_t i = start; i < end; ++i) {
            shifted[i] = source[i - nshift];
        }
    }

    // Assign.
    if (!memnull(source)) { memdelete(source); }
    source = shifted;

    // Return new alloc size.
    return alloc_size;
}

// Shift memory.
// Produces segfault when "nshift" is larger then "size" or smaller then "-size"
// Automatically resizes the array when required.
// A positive "nshift" value shifts the array to the right from the "start" index.
//   Therefore the array's size will be larger.
//   Thus "memshift("Hello", ..., 1, 1)" will return "Heello World!".
// A negative "nshift" value shifts the array to the left from the "start" index.
//   Therefore the array's size will be smaller.
//   Thus "memshift("Hello World!", ..., -1, 1)" will return "Hllo World!".
// Everything before index "start" and after index "end" remains the same.
// Shifts nothing when "start" is out of range.
// Automatically reverts "end" to "size" when the value is "0" / out of range.
// Best leave "end" "0", otherwise keep in mind that "end" must be "x + nshift" with x as your end.
// Optionally specify "alloc_size" to set the newly allocated size, automatically increases when the specified value is too small.
// Returns the newly allocated size of the memory.
template <class T>
slong_t memshift(T* source, slong_t size, int nshift, index_t start = 0, index_t end = 0, slong_t alloc_size = 0) {
    start = start.subscript(size);
    if (start == npos) {
        if (alloc_size == 0) { return size; }
        return alloc_size;
    }
    if (end > size + nshift) { end = size + nshift; }
    else if (end == 0) {
        end = start.subscript(size);
        if (end == npos) { end = size + nshift; }
    }
    if (end > alloc_size) { alloc_size = end; }
    else if (alloc_size == 0) { alloc_size = size; }
    return fastmemshift(source, nshift, start, end, alloc_size);
}

// Pop an item.
// Supports negative index.
// Returns "T()" when the index is out of range.
template <class T>
T mempop(T* source, slong_t size, index_t index, slong_t alloc_size = 0) {
    if (index == 0) {
        T x = source[0];
        ++source;
        return x;
    }
    if (alloc_size == 0) { alloc_size = size; }
    index = index.subscript(size);
    if (index == npos) { return T(); }
    T x = source[index];
    fastmemshift(source, -1, index, size - 1, alloc_size);
    return x;
}

// Pop an item.
// Supports negative index.
// Returns "def" when the index is out of range.
template <class T>
T mempop(T* source, slong_t size, index_t index, T& def, slong_t alloc_size = 0) {
    if (index == 0) {
        T x = source[0];
        ++source;
        return x;
    }
    if (alloc_size == 0) { alloc_size = size; }
    index = index.subscript(size);
    if (index == npos) { return def; }
    T x = source[index];
    fastmemshift(source, -1, index, size - 1, alloc_size);
    return x;
}

I am using macOS with compiler clang++.

\$\endgroup\$
9
  • \$\begingroup\$ This code uses templates, which makes it invalid C code. \$\endgroup\$ Commented Jul 15, 2022 at 17:32
  • 1
    \$\begingroup\$ It’s not particularly valid C++ code either. <stdio.h> is a C header, not a C++ header. And there are some types in there which have no definition (slong_t, index_t). \$\endgroup\$
    – indi
    Commented Jul 16, 2022 at 3:20
  • 1
    \$\begingroup\$ @indi <stdio.h> is a standard header in C++. It’s been deprecated since 1998, but will officially never be removed. \$\endgroup\$
    – Davislor
    Commented Jul 16, 2022 at 6:49
  • 1
    \$\begingroup\$ “It is both a C and a C++ header…” The C++ standard literally contradicts you. I’ll continue quoting the standard, thank you. \$\endgroup\$
    – indi
    Commented Jul 16, 2022 at 14:48
  • 1
    \$\begingroup\$ Incidentally: “(#include <stdio.h> lets you do both.)”. No, this is incorrect, too. <stdio.h> ONLY guarantees printf()NOT std::printf(). An implementation may provide std::printf() in <stdio.h>… but they don’t have to. (Similarly, an implementation MAY provide printf() in <cstdio>… but they don’t have to. They must provide std::printf() in <cstdio>, though.) \$\endgroup\$
    – indi
    Commented Jul 16, 2022 at 14:51

1 Answer 1

6
\$\begingroup\$

There are a lot of problems with this code that go far beyond the simple issue of leaking memory, and most of them stem from not understanding what objects are in C++, and how object lifetime works.

Let’s start at the top to illustrate:

template <class T>
T* memalloc(slong_t alloc_size) {
    T* ptr;

    unsigned long n = (unsigned long) alloc_size;
    ptr = (T* ) malloc((n + 1) * sizeof(T));

    return ptr;
}

(I stripped the commented code out for clarity.)

You seem to be confusing C and C++, and thinking that all you have to do is allocate memory, and then, poof, you have Ts. That may work in C; it is not how C++ works at all.

When you create an object in C++, (at least) two things happen:

  1. memory for that object gets allocated; then
  2. the object gets instantiated in that memory.

I assume malloc() is supposed to be std::malloc(). In that case, it allocates memory… but it doesn’t instantiate anything. That’s bad.

Basically, what this function is does is allocate space for alloc_size number of Ts… but then just returns that spaceNOT actually any Ts, just a space full of garbage. It may appear to work okay for simple types like int and char (but it is still very much wrong)… but for complex types, it will probably crash (a crash is the most likely outcome, but literally anything could happen).

Here’s how to properly allocate and instantiate an array of T:

template <class T>
T* memalloc(slong_t alloc_size) {
    // T* ptr;  // Don't declare your variables at the top of the function
                // like this. That's archaic practice even in C.

    auto n = static_cast<unsigned long>(alloc_size);    // Don't use C-style
                                                        // casts.
                                                        // Also, why take
                                                        // alloc_size as
                                                        // "slong_t" when you
                                                        // really want it to
                                                        // be unsigned long?
                                                        // Why not just take
                                                        // an unsigned long?

    // ptr = (T* ) malloc((n + 1) * sizeof(T)); // Don't use C-style casts.
                                                // And if you were asked for
                                                // n elements, why allocate
                                                // n + 1?

    // Note that you are not taking alignment into account... but that's
    // *kinda* okay here, because std::malloc() allocates at max alignment
    // (which is wasteful, but at least not incorrect).

    auto ptr = static_cast<T*>(std::malloc(n * sizeof(T)));

    // std::malloc() may fail. You should check that:
    if (!ptr)
        throw std::bad_alloc{};
        // or return nullptr;

    // Alright, you have allocated the *space* for n T objects... but no T
    // objects exist yet. We have to create them in that space.

    // In theory, all we have to do is this:
    //  for (auto i = decltype(n){}; i != n; ++i)
    //      new (ptr + i) T;

    // In practice, there are two complications.
    //  1.  This will default-initialize... which is fine *sometimes*, but
    //      dangerous, because it can create indeterminate values. *Writing*
    //      over indeterminate values is fine. *Reading* them is not. So as
    //      as long as you always overwrite the returned objects, it's fine.
    //      But you're leaving the door open for bugs.
    //  2.  This doesn't take into account the fact that construction might
    //      fail, and throw an exception.

    // To take exceptions into account, you'd have to do something like this:
    auto i = decltype(n){};
    try
    {
        for (; i != n; ++i)
            new (ptr + i) T;
    }
    catch (...)
    {
        // Something failed. So we need to destroy all the objects we already
        // constructed. Luckily, we've been keeping track with i.
        while (i != 0)
            (ptr + i - 1)->~T();

        // Now that all the objects have been cleaned up, it's time to free
        // the memory.
        std::free(ptr);

        // At this point you can either rethrow the exception with:
        throw;
        // OR, return nullptr:
        return nullptr;
    }

    // If you got here, you have successfully allocated the memory, *AND*
    // filled it up with T objects. Now it is safe to return.

    return ptr;
}

You can simplify all that by using library functions:

template <class T>
T* memalloc(slong_t alloc_size)
{
    auto n = static_cast<unsigned long>(alloc_size);

    auto ptr = static_cast<T*>(std::malloc(n * sizeof(T)));
    if (!ptr)
        throw std::bad_alloc{};

    try
    {
        std::ranges::uninitialized_default_construct_n(ptr, n);

        // Note that the above default-initializes all the T objects, which,
        // as I explained above, is dangerous.
        // The safer option is:
        //  std::ranges::uninitialized_value_construct_n(ptr, n);
    }
    catch (...)
    {
        // Something failed to construct. Luckily, the functions above already
        // did all the cleanup... except we still need to free the memory.
        std::free(ptr);

        throw;
        // or:
        //  return nullptr;
    }

    return ptr;
}

So, to summarize:

  • You can’t simply allocate memory and then think you have a bunch of objects. In C++, raw memory is just raw memory; it’s just a bunch of garbage bits.
  • Allocating memory only means that you now own those garbage bits… they’re yours and you can do whatever you want with them… but they’re still just garbage bits.
  • To turn those garbage bits into useful objects, you need to actually construct the objects in that memory.

And of course, when cleaning up, you need to do the reverse process:

template <class T>
void memdelete(T*& source) {
    free(source);
    source = NULL;
}

Here you are freeing the memory, but not actually destroying the objects first. You’re just pulling the rug out from under those objects, which for anything but the most trivial types is probably going to cause a crash. Or worse.

Here’s what you should do:

template <class T>
void memdelete(T*& source)
{
    // Taking the pointer by reference just to you can set it nullptr (note,
    // nullptr... *NOT* NULL... NULL is a C thing, not a C++ thing) is both
    // pointless and wrong.
    // The reason it's not wrong is because, well, try this:
    //  int* const p = memalloc(5);
    //  memdelete(p);

    // So, to properly clean up, first we have to destroy all the objects.
    // The problem is... you don't know how many objects there are!
    // This is why we have both `new` and `new[]` in C++. Plain `new`
    // allocates only a single object worth of memory, but `new[]` allocates
    // multiple objects' worth, and pays for that by storing the amount of
    // objects allocated somehow.

    // So let's assume your memalloc() function somehow stores the number of
    // objects somewhere, and we can retrieve it:
    auto n = ...;

    // Now we can destroy those T objects:
    for (auto i = decltype(n){}; i != n; ++i)
        (source + i)->~T();

    // Or, better, using libary functions:
    std::ranges::destroy_n(source, n);

    // NOW it is okay to finally free the memory:
    std::free(source);

    // As I said, this is pointless, and wrong:
    source = NULL;
}

So you see, you have far, far deeper problems in your code that merely leaking memory. Pretty much all of your functions are just plain broken in C++. They might work in C (other than that they’re templates, of course), but they are just plain wrong in the C++ object model.

Most of the other functions here aren’t really memory allocation functions, they’re algorithms. Like, memswap() doesnt’t allocate or deallocate any memory; it’s just a less useful std::iter_swap(). And a lot of those functions that do allocate memory… really shouldn’t. Like memreverse() is basically just an allocation followed by a copy-backwards… but it makes little sense given either std::ranges::reverse() or std::ranges::views::reverse.

There’s really no point in me going through all the rest of the functions, because they’re all broken for more or less the same reason: they are basically C functions trying to play in the grown-up C++ world, and failing because of it. C++ is not just C with classes and templates, it is an entirely different language with and entirely different philosophy and rules. Sometimes the differences are very, very subtle, but they are there.

So you really need to go back to the drawing board, and either write C, or write C++. There is no such language as “C/C++”.

  • If you’re writing C, I suppose most of the code is fine (though I’m not a C expert, so 🤷🏼). Obviously you need to remove the template stuff and any other C++ stuff, of course.
  • If you’re writing C++, you need to start by learning the C++ object model and the C++ virtual machine. If that sounds like a lot of work… well, yeah, you are trying to do very low-level stuff. You can’t expect that to be easy. If you want to learn C++, obviously don’t do that by starting with the intensely difficult and nuanced low-level stuff. Start with the easy, high-level stuff.

(Extra) Why <stdio.h> is not a C++ header

I made a comment that <stdio.h> is not a C++ header, but I didn’t address that in the review, and I got some push back in comments. So let me fix that missing part of my review, and explain why <stdio.h> is not a C++ header, and why you should (almost) never use it in C++ code.

For starters, there seems to be some misconception that <stdio.h> and <cstdio> are somehow the same header, and can be used more or less interchangeably. The misconception seems to be that you can include either, and then either use C library functions as meow() or std::meow(), and there will be no difference.

Every part of that is wrong.

First, <stdio.h> and <cstdio> don’t have to be the same file, and almost never are. For example, on my system, #include <stdio.h> will get you /usr/include/stdio.h, while #include <cstdio> will get you /usr/include/c++/11/cstdio. (That’s for libstdc++. For libc++, there are also two different files: stdio.h and cstdio, both in /usr/lib/llvm-11/include/c++/v1.)

The idea that you could include <stdio.h> and somehow get both printf() and std::printf() is just… bizarrely wrong, and seems to fundamentally misunderstand what the whole point of allowing C library headers is about. The whole idea is that you could theoretically have a system that has a standard C library installed, and you could build a C++ implementation next to it without changing the C implementation. If it were required that <stdio.h> included namespace std, that would violate that whole principle. So obviously including <stdio.h> cannot guarantee providing std::printf(). It may… but it doesn’t have to.

What about the inverse: does including <cstdio> promise both ::printf() and std::printf()? The standard in [headers] paragraph 5 says:

{...} the contents of each header cname is the same as that of the corresponding header name.h as specified in the C standard library. In the C++ standard library, however, the declarations (except for names which are defined as macros in C) are within namespace scope of the namespace std. It is unspecified whether these names {...} are first declared within the global namespace scope and are then injected into namespace std by explicit using-declarations {...}.

So the answer is no. <cstdio> MAY include ::printf()… but doesn’t have to.

So the bottom line is this:

  • If you include <stdio.h>, you are guaranteed printf()… and you might get std::printf(), but might not.
  • If you include <cstdio>, you are guaranteed std::printf()… and you might get ::printf(), but might not.

So, no, the two headers are not equivalent, and cannot be used interchangeably.

But there’s more.

Another part of the misconception has that the difference between meow() and std::meow() is simply a matter of style. This is wrong, too.

To understand why, you have to understand the concept of calling conventions. The calling convention determines how std::meow(1, 2, 3) gets transformed into machine code. Some of the things that the calling convention determines include:

  • How the name std::meow is transformed into a specific function identifier. For example, in the C calling convention, meow(1, 2, 3) may call meow or _meow, while in C++, std::meow(1, 2, 3) may call _ZSt4meowiii.
  • The order that the arguments are passed (left-to-right, or right-to-left).
  • Where the arguments are passed (registers versus the stack).
  • Who is responsible for cleaning up the arguments.
  • … and so on.

The C++ standard assumes at least two calling conventions: C and C++. The most obvious difference between the two is that the C++ calling convention does name mangling. How does the compiler determine the difference between calling meow(1, 2, 3) with ints and meow(1.0, 2.0, 3.0) with doubles? With GCC, on x86-64 platforms, the former is _Z4meowiii, the latter is _Z4meowddd. But if meow() were a C function, it couldn’t be overloaded. With GCC, on x86-64 platforms, it would just be meow.

There may be other differences between the C and C++ calling conventions. For example, the C++ calling convention may include passing this in a specific register. Or the two calling conventions may be identical—the C calling convention could even include name mangling.

But the C++ standard assumes that the two calling conventions are different, to the point that you are not even supposed to mix function pointers to C and C++ functions.

Why does that matter? Well, here’s the magical question: meow() in <foo.h> must obviously use the C calling convention… but does std::meow() in <cfoo>? Standard (in [library.requirements.usage.linkage] paragraph 2) says: … meh 🤷🏼. So maybe not!

What that means is that you cannot assume that printf() (in <stdio.h>) is the same function as std::printf() (in <cstdio>). They might be. Or even if they’re different, one might ultimately end up calling the other. But they might be entirely different functions, with entirely different calling conventions. putchar() may call _putchar in cruntime.so by passing the argument on the stack, and std::putchar() may call _ZSt4putchari in c++-runtime.so by passing the argument in register. The standard allows for all of that.

(There are also other implications. For example, if you define a function in C++ as foo(auto (*)(int) -> int), you can’t call it like foo(::putchar) (because ::putchar() has C linkage, whereas foo is using C++ linkage)… and you probably can’t call it like foo(std::putchar) (because std::putchar() might have C linkage, plus other reasons). To take C function by function pointer, you need to use a C linkage function pointer.)

Yes, in practice, on all platforms I’ve ever heard of, the only difference between the C and C++ calling conventions is the name mangling, so in practice you can usually get away with mixing C function pointers and C++ function pointers. But it’s still conceptually wrong. And you may get burnt. This is just one of the many, many hazards of mixing C and C++… which is why you should never do it unless you absolutely positively must must must have to.

But even on common platforms, there can be a real difference between meow() and std::meow()… even when they are (nominally) the exact same function. For example, the C library sqrt() has only a single flavour: it takes a double. The C++ sqrt() comes in multiple flavours; at least float, double, and long double. Let’s assume the double version just maps exactly to the C library function… even so, the float and long double versions may not. So if you call double result = ::sqrt(float_value), it may have to convert that float value to a double, then call the C library function… whereas if you call double result = std::sqrt(float_value), it may not convert the argument, and instead call the float version of the function in the C++ library, and then convert the return value to a double. That may give very different results.

Okay, so all of the above explains why <meow.h> and <cmeow> are NOT the same thing—and why, by extension, the C library meow() and the C++ library std::meow() may be totally different, and may produce different behaviour even if not. But it doesn’t prove you should never use <meow.h>. I mean, it’s included in the C++ standard, so it’s cool to use, right?

Wrong.

Just because it’s included in the standard, doesn’t mean that it’s a good idea to use. Especially when the standard itself literally says don’t use it.

Does it though? Does the standard actually say “don’t use it”? Well, the C headers were deprecated for a long time, which is a pretty clear “don’t use it” message… buuuut… as of C++23, they re going to be “un-deprecated”. (The reason why is simply because the C++ standard committee essentially gave up, and accepted that we’ll be interacting with C code basically forever.) But does the fact that they’re un-deprecated mean they’re okay to use now?

No.

In [headers], there’s a footnote that says “… the newer forms are the preferred forms for all uses except for C++ programs which are intended to be strictly compatible with C.” So, “preferred”. But does “not preferred” mean “don’t use it”? I’d say yes, but if you want to argue, then I would point you to [support.c.headers.general] paragraph 1, which says, and I quote:

Source files that are not intended to also be valid ISO C should not use any of the C headers.

So, there you go. “You should not do X” is standardese for “don’t do X”. This is as clear a statement as you’re going to get from the standard: don’t use them.

Unless this source code is intended to be valid C… which it clearly ain’t, because it’s all templates… then using <stdio.h> is WRONG. It is not a style choice. It (potentially) changes the behaviour of the program (because the C library function may not be the same the C++ one, and even when it is, there may be other factors). It is objectively incorrect in C++ code.

Do not use C headers in C++ code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.