Open-source project
eteran/c-vector avatar
eteran/c-vector

c-vector hides the vector metadata in front of the allocation

A dynamic array implementation in C similar to the one found in standard C++

896 stars116 forksCMIT

At a glance

What is it?
c-vector is a std::vector style growable array for plain C89, implemented entirely in macros and therefore header only. The trick it uses is the one allocators use, allocating more than requested and using the padding in front for the size, the capacity and an element destructor, so the user's pointer still points at the first element and the total overhead is a fixed number of bytes per vector.
Who is it for?
Adopt c-vector if you are writing C89 code, or C that has to be compiled on toolchains that predate C99, and you want a vector with the ergonomics of the standard C++ container without pulling a C++ compiler into the build. Do not adopt it if your project can use C++ at all, because std::vector gives you the same operations with real types, real destructors and no macro expansion, and do not adopt it in code that must be reviewed by people who dislike macro heavy headers.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 32 days ago.
What is it written in?
Mainly C, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The padding trick, and what it costs per vector

The design of c-vector is a single idea applied carefully, and it is worth understanding before the API, because everything else follows from it.

The library is described as an implementation of a std::vector like growable array in plain C89 code, and the result is a type safe, easy to use dynamic array with a familiar set of operations. Type safe in C means the element type is baked into the macro names at compile time, so a vector of floats and a vector of ints are not interchangeable, and a mistake is a compile error rather than a crash.

The memory trick is the one many allocators use. The library allocates slightly more data than requested and uses the extra padding in the front as storage for meta-data, so any non-null vector looks like this in memory:

bash
	+------+----------+-----------------+---------+
	| size | capacity | elem_destructor | data... |
	+------+----------+-----------------+---------+
	                                    ^
	                                    | user's pointer

The user is given a pointer to the first element of the data, and the code has trivial access to the meta-data without the user having to know about it. The total overhead is stated as 2 * sizeof(size_t) + sizeof(void (*)(void *)) per vector. In other words, the size, the capacity and the function pointer for destroying elements sit in the allocation, in front of the elements.

The consequence for a user is that a vector is a plain pointer to the elements, with no wrapper struct to dereference. Indexing, which is the hot operation, compiles to exactly the same code as indexing an array. The cost is that the header must be able to reach back before the pointer it gave you, which is why the library is built out of macros that know the element type and can reconstruct the header.

The elem_destructor field is the one that shows the design is aimed at C++ style lifetimes in a C library. It is a pointer to a function that knows how to destroy one element, so the vector can clean up elements of a non trivial type when it is freed.

Growth is doubling or linear, and the choice is a compile time define

Before you use the library you are expected to decide how it grows, and the decision is made by a preprocessor define rather than a runtime option.

The header comment in the README example explains both policies. If CVECTOR_LINEAR_GROWTH is defined, the vector increases in capacity by one each time it runs out of space. If it is not defined, the vector is liberal and doubles in capacity each time it runs out of space. Having it defined minimizes how much unused space is allocated.

bash
#define CVECTOR_LINEAR_GROWTH

#include "cvector.h"
#include <stdio.h>

The two policies are the classic space against time trade-off, and the framing in the comment is revealing: doubling is called liberal, linear is called minimizing. Doubling means an amortised constant cost per append and a maximum of twice the needed memory. Growing by one means the memory is exact and the append is a reallocation every single time.

For most uses, doubling is the right answer, because the amortised cost of the occasional reallocation is far below the cost of copying the whole array on every push. Linear growth only makes sense when the size is known in advance or when the memory is very tight and the vector is filled once.

The fact that this is a compile time define rather than a runtime flag is worth noting for a library that is header only. Because the macros expand at the point of inclusion, a translation unit that includes the header with the define and another that includes it without the define get different growth behaviour, and the two cannot share a vector safely. In a small project that is not a problem, but in a codebase with many translation units it is the kind of thing to decide once and enforce in the build.

Using it looks like the C++ container, spelled with macros

The example in the README is a complete program, and the API in practice is short.

The vector itself is declared with a macro that takes the element type:

bash
	/* add some elements to the back */
	cvector_push_back(v, 10);
	cvector_push_back(v, 20);
	cvector_push_back(v, 30);
	cvector_push_back(v, 40);

	/* remove an element by specifying an array subscript */
	cvector_erase(v, 2);

	/* remove an element from the back */
	cvector_pop_back(v);

The declaration itself, cvector_vector_type(int) v = NULL, uses a macro rather than a type, and the README points out the alternative spelling cvector(int) v for when you prefer to write the type once. A NULL vector has a size and capacity of 0, and vector_begin and vector_end return NULL on a NULL vector, so the empty case needs no special handling beyond the usual null check.

Two iteration styles are shown in the example, and both work. The first walks the vector with cvector_begin and cvector_end as pointers, the iterator style. The second indexes it with the standard array syntax, v[i], while looping on cvector_size. The fact that plain indexing works is a direct consequence of the layout: the user holds a pointer to the first element, so v[i] is just pointer arithmetic.

The full set of operations is given in a table in the README that maps each std::vector method to its cvector equivalent, which is the most efficient way to read the API. Destructor becomes cvector_free, at becomes cvector_at, front and back become cvector_front and cvector_back, empty, size, capacity, clear, insert, erase, push_back, pop_back, reserve, resize, data and swap all have macro equivalents.

Two API gaps: resize needs a value and swap needs the type

Two departures from the standard C++ API are called out in the README, and both are the kind of thing that shows up as a compile error rather than a bug, which is a good way to find out about it.

The first is cvector_resize. It requires a value argument to initialize any newly added elements. In C++ you can call resize with a count and rely on the element type being default constructible, and with a count and a value to fill. Here the value is not optional, so every resize has to say what the new elements should be, which for a type without a sensible default is a requirement rather than a limitation.

The second is cvector_swap. It requires the element type as its third argument, with int for the cvector(int) vectors shown in the table. Since the macro already knows the type of the first operand, requiring it again for the second is redundant from the caller's point of view, and it is the kind of repetition that macros make easy to get wrong when the two vectors have different element types, since the compiler will not stop you.

Neither of these is a reason not to use the library, and both are consequences of the macro only design. A macro cannot overload on argument count in the way an overloaded C++ function can, so the extra type argument is how the second vector's element type is communicated to the expansion.

The other gap worth noting is a missing one. The API table covers copy construction with cvector_copy, and there is no move semantics, no initializer list construction and no exception guarantee, because none of those exist in C89. A project that needs those should be using C++.

A header, a utilities header and a test suite that is part of the tree

The repository is small, and the file listing describes it well.

There is cvector.h, the library itself, and cvector_utils.h, which is a separate utilities header rather than part of the main one. Splitting the utilities out is the usual way to keep a widely included header free of code that most users do not need.

Then there is the code that keeps the library honest. test.c and unit-tests.c sit at the root, along with a utest/ directory, which is the unit test framework the tests are written against. example.c is the compiled version of the example program in the README, so the documentation and the build share the same source.

The build is CMake based, with a CMakeLists.txt at the root, and there is a .clang-format file, so the source is formatted with a specific style rather than by hand, alongside an .editorconfig and a .gitattributes. The CI surface is visible in the badges at the top of the README: one workflow runs CMake, and a second runs CodeQL analysis. The CodeQL job in particular is a sensible thing to have on a library that does pointer arithmetic and manual memory management, and it is a reminder that this code is exactly the kind that a static analyser should be pointed at.

For a user, the practical consequence of this layout is that the whole library is one header you copy or vendor, and the tests are there to read if you want to know what the macros actually do in each case.

The alternative is std::vector, and the approach difference is the language

The honest comparison is with the C++ standard library, because the README is written as a mapping to it.

The difference in approach is that std::vector is a class. It has a real type, real constructors, a real destructor, iterators that are class types with iterator traits, and a size and capacity that the compiler can reason about. A vector is a value that can be copied, moved, returned from functions and stored in other containers. The C++ compiler enforces the type of a vector, so a vector of ints cannot be passed where a vector of floats is expected.

c-vector reaches similar ergonomics in a language that has none of that machinery, and it pays for it in three ways. Everything is a macro, so error messages come from the preprocessor rather than the compiler, and the type is repeated in the macro name, which is verbose and, in the case of cvector_swap, occasionally redundant. The element destructor is a function pointer stored in the allocation, so a vector of a type with a non trivial destructor needs the right function passed in, and getting that wrong is a memory bug rather than a compile error. And there is no ownership semantics, so the rules about who frees what are conventions rather than types.

There is also a sibling project using the same approach. The README notes that if you like this library, german-one has created a string library using this approach, hosted at the c-string repository. That is worth knowing because it shows the padding trick generalises to a string type, and it is a way to get dynamic strings in C89 without a separate allocation strategy.

So the decision is simple. If C++ is available, use std::vector and stop reading. If you are in C89 for a reason, and the reason is a toolchain rather than a preference, c-vector is a reasonable way to get the container you would otherwise have to write yourself.

MIT licensed, no releases, and a small header to review

The licence is MIT, with the LICENSE file at the repository root, which is about as permissive as a library licence gets and imposes little beyond keeping the notice.

The repository is not archived and the last push was on 2026-09-21, so the code is still being touched. There are no GitHub releases, which is normal for a single header library that is consumed by copying it into a project rather than by installing a version from a package feed. The version you use is the version you vendored.

That last point is the one to keep in mind when adopting. A header only C library with no releases and no package manager entry is not something you can depend on with a version constraint, so the usual questions about upgrade cost apply. Do you track upstream, and how do you notice a change? Do you vendor a copy, and who updates it? Is the copy in your tree subject to your own review and security process?

The counter argument is that there is almost nothing to review. One header of macros, with an example, a test suite and a CodeQL job watching it. The code is small enough that reading it is faster than trusting it, and the API table in the README is a complete map of what the macros do.

The last detail to keep in mind when deciding is the C89 constraint. The library is written in plain C89, which is what makes it usable in the environments it is meant for, and it is also what rules out compound literals, designated initialisers, variable length arrays and everything else a modern compiler would let you use. If your project is not constrained to C89, this library is solving a problem you do not have.

Editorial conclusion

Adopt c-vector if you are writing C89 code, or C that has to be compiled on toolchains that predate C99, and you want a vector with the ergonomics of the standard C++ container without pulling a C++ compiler into the build. Do not adopt it if your project can use C++ at all, because std::vector gives you the same operations with real types, real destructors and no macro expansion, and do not adopt it in code that must be reviewed by people who dislike macro heavy headers. Before you commit, read cvector.h to see how the macros are implemented, decide whether to define CVECTOR_LINEAR_GROWTH for your access pattern, and note the two places the API departs from std::vector, since cvector_resize needs a value and cvector_swap needs the element type as a third argument.

Frequently asked questions

What is c-vector and what does it implement?

It is a std::vector like growable array for plain C89, implemented entirely in macros and therefore header only. It provides a type safe dynamic array with a familiar set of operations, and the API table maps each std::vector method to its cvector equivalent.

How does c-vector store its size and capacity?

It allocates slightly more than requested and uses the padding in front of the data for the size, the capacity and the element destructor. The total overhead is 2 * sizeof(size_t) + sizeof(void (*)(void *)) per vector, and the user's pointer still points at the first element.

What does CVECTOR_LINEAR_GROWTH change?

Without it the vector doubles its capacity each time it runs out of space. With it defined, the vector grows by one element instead, which minimizes unused space at the cost of reallocating on every append.

How do I declare a cvector of a given type?

With the cvector_vector_type macro, for example cvector_vector_type(int) v = NULL, or with the cvector(int) v spelling. A NULL vector has a size and capacity of 0, and cvector_begin and cvector_end return NULL for it.

What are the c-vector API gaps against std::vector?

cvector_resize requires a value argument to initialize any newly added elements, and cvector_swap requires the element type as its third argument. The README calls both out explicitly.

Official sources

  1. eteran/c-vector on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/eteran-c-vector.svg)](https://hysenlabs.com/projects/eteran-c-vector)

Was this useful?