You cannot expose a template to C, and any object created in C code will not have member functions. So C code cannot define an instance of your C++ queue. However, you can still leverage C++ templates to avoid code duplication.
Note (for future readers): A convenient way to use C++ objects in C code is to allocate the object dynamically so that the type can be incomplete in the C code. The question specifically rules this out, though.
It might be better to think in terms of function templates instead of struct templates. This is one of the situations where forcing an object-oriented design can be more trouble than it is worth. Implement the functionality with function templates (perhaps inside a namespace to group them), and have the C API call those. The C++ version of the queue could also call these functions, if you still want a C++ version. Admittedly, such a setup does not look great from a C++ perspective, but some amount of compromise is necessary to accommodate C.
With this approach, structures like Qint8 would not be exposed to C; they would be defined in C. The API functions would be declared in C, but defined in C++ where they can utilize the function templates. To get started with these templates, let's have a way to recognize that a structure looks like it could be a queue (QueueLike) and a way to extract the element type and size (QueueInfo).
#include <concepts>
#include <type_traits>
#include <utility>
#include <cstddef> // If you want to force the indices to be `size_t`
#include <limits> // If you do not want to force the indices to be `size_t`
template <typename Q>
concept QueueLike =
requires (Q queue) {
requires 0 < std::extent_v<decltype(queue.data)>; // is an array
// If you want to force the indices to be `size_t`:
{queue.readIdx} -> std::same_as<size_t&>;
{queue.writeIdx} -> std::same_as<size_t&>;
// If you do not want to force the indices to be `size_t`:
// The indices need to be integers of the same type and large enough for the data.
requires std::is_integral_v<decltype(queue.readIdx)>;
requires std::same_as<decltype(queue.writeIdx), decltype(queue.readIdx)>;
requires std::extent_v<decltype(queue.data)>
<= std::numeric_limits<decltype(queue.readIdx)>::max();
};
template <QueueLike Q>
struct QueueInfo
{
using data_type = decltype(std::declval<Q>().data); // for readability
using element_type = std::remove_extent_t<data_type>;
static constexpr auto max_len = std::extent_v<data_type>;
};
Given these tools, one can write function templates to implement the queue functionality. These should be minor adaptations of the Queue member functions. Unlike the code in the question, where the template parameters are the type of elements and the maximum length, the template parameter here is the struct that holds the data. The type of the elements and the maximum length are obtained via the QueueInfo template. As examples, here are an implementation of queue initialization and a sketch of an implementation of "push back". (Presumably the details of "push back" are known; if not, that's a separate question.)
template <QueueLike Q>
void InitializeQueue(Q * queue) {
if ( queue != nullptr )
queue->readIdx = queue->writeIdx = 0;
}
template <QueueLike Q>
bool PushBackImpl(Q * queue, typename QueueInfo<Q>::element_type value) {
// Make sure `queue` is non-null and `*queue` is not full before pushing.
// The maximum length is available as `QueueInfo<Q>::max_len`.
queue->writeIdx %= QueueInfo<Q>::max_len; // Probably done at some point.
// Do whatever else is needed.
return true;
}
Once you have all the templates you need, the functions of the C API become simple wrappers. As a reminder, these definitions need to be in a C++ source file so that the C compiler does not try to process them. You might note that the template arguments have been deduced. This is intentional, as it cuts down on copy-paste errors. (To change the Qint8 functions to Qchar4 versions, only the function names and parameter types need to be changed, not the function bodies.) Also, I added an initialization routine since a C compiler will not invoke C++ constructors.
void Qint8_Initialize(Qint8 * queue) {
InitializeQueue(queue);
}
bool Qint8_PushBack(Qint8 * queue, int value) {
return PushBackImpl(queue, value);
}
bool Qint8_PopFront(Qint8 * queue, int * value) {
return PopFrontImpl(queue, value);
}
Since the project is mainly C code, it might be that the C++ queue is not really needed. If it is, though, it can leverage the above function templates. If the data members are kept public (not the best idea), the implementation could look like the following.
template <typename T, size_t MAX_LEN>
struct Queue {
T data[MAX_LEN];
size_t readIdx = 0;
size_t writeIdx = 0;
bool PushBack(T value) { return PushBackImpl(this, value); }
bool PopFront(T * value) { return PopFrontImpl(this, value); }
};
It gets a little more complicated when the data members are private, but the point still stands that there is no code duplication. (Well, the initialization is duplicated, but you could add a constructor that calls InitializeQueue if you want.) You get object-oriented queues for C++ code and procedure-based queues for C, with the logic shared between them.
structinstances for C like you've done in your sample. And if you do that, why use templates at all for C++ (DRY) You may have to implement something likeextern "C" Qchar4 *createQchar4()andextern "C" void freeQchar4( Qchar4 * )functions using opaquestructs.Qint8that uses aQueue<int, 8>under the hood? In order to do that, your C code would have to invoke C++ code, which means your C++ code would have to instantiateQueue<int, 8>.