14

When designing a C++ library, I read it is bad practice to include standard library containers like std::vector in the public interface (see e.g. Implications of using std::vector in a dll exported function).

What if I want to expose a function that takes or returns a list of objects? I could use a simple array, but then I would have to add a count parameter, which makes the interface more cumbersome and less safe. Also it wouldn't help much if I wanted to use a map, for example. I guess libraries like Qt define their own containers which are safe to export, but I'd rather not add Qt as a dependency, and I don't want to roll my own containers.

What's the best practice to deal with containers in the library interface? Is there maybe a tiny container implementation (preferably just one or two files I can drop in, with a permissive license) that I can use as "glue"? Or is there even a way to make std::vector etc. safe across .DLL/.so boundaries and with different compilers?

Community
  • 1
  • 1
jdm
  • 9,470
  • 12
  • 58
  • 110

4 Answers4

4

You can implement a template function. This has two advantages:

  1. It lets your users decide what sorts of containers they want to use with your interface.
  2. It frees you from having to worry about ABI compatibility, because there is no code in your library, it will be instantiated when the user invokes the function.

For example, put this in your header file:

template <typename Iterator>
void foo(Iterator begin, Iterator end)
{
  for (Iterator it = begin; it != end; ++it)
    bar(*it); // a function in your library, whose ABI doesn't depend on any container
}

Then your users can invoke foo with any container type, even ones they invented that you don't know about.

One downside is that you'll need to expose the implementation code, at least for foo.

Edit: you also said you might want to return a container. Consider alternatives like a callback function, as in the gold old days in C:

typedef bool(*Callback)(int value, void* userData);
void getElements(Callback cb, void* userData) // implementation in .cpp file, not header
{
  for (int value : internalContainer)
    if (!cb(value, userData))
      break;
}

That's a pretty old school "C" way, but it gives you a stable interface and is pretty usable by basically any caller (even actual C code with minor changes). The two quirks are the void* userData to let the user jam some context in there (say if they want to invoke a member function) and the bool return type to let the callback tell you to stop. You can make the callback a lot fancier with std::function or whatever, but that might defeat some of your other goals.

John Zwinck
  • 239,568
  • 38
  • 324
  • 436
4

Actually this is not only true for STL containers but applies to pretty much any C++ type (in particular also all other standard library types).

Since the ABI is not standardized you can run into all kinds of trouble. Usually you have to provide separate binaries for each supported compiler version to make it work. The only way to get a truly portable DLL is to stick with a plain C interface. This usually leads to something like COM, since you have to ensure that all allocations and matching deallocations happen in the same module and that no details of the actual object layout are exposed to the user.

ComicSansMS
  • 51,484
  • 14
  • 155
  • 166
  • Maybe not *all* the standard library types--I bet you could get away with std::pair, array, bitset, and tuple on a bunch of toolchains. But it surely isn't guaranteed--I'm just brainstorming what parts of the stdlib might not be toxic. – John Zwinck Jan 17 '14 at 12:33
  • @JohnZwinck: I would **not** bet on `tuple`; libstdc++ and libc++ versions differ widely if I recall correctly. – Matthieu M. Jan 17 '14 at 12:39
  • @ComicSansMS: Yeah, I just thought about COM and wondered how they solved the problem there... which leads down the rabbit hole of VARIANTs and SAFEARRAYs. The more I learn about COM, the more I realize it is so complicated for a reason. – jdm Jan 17 '14 at 12:39
  • 1
    @jdm It's a deep rabbit hole indeed and the number one reason I avoid dynamic libraries in C++ like the plague. The easiest solution is always to ship your library in source code and have the customer compile it with their toolchain. Unfortunately in many cases this is not an option. Hopefully we will eventually get a standardized module system in ISO C++ that takes care of these issues. – ComicSansMS Jan 17 '14 at 12:48
  • @jdm COM deals with it by defining their own standard types that are built out of C structures. To interop with non-C++ code (and with c++ code compiled by a different compiler!) requires you use something that's standardised - C types seem to be the only option. – gbjbaanb Jan 17 '14 at 13:05
  • @ComicSansMS: I am afraid this is completely out of scope of the module system. – Matthieu M. Jan 17 '14 at 13:36
4

TL;DR There is no issue if you distribute either the source code or compiled binaries for the various supported sets of (ABI + Standard Library implementation).

In general, the latter is seen as cumbersome (with reasons), thus the guideline.


I trust hand-waving guidelines about as far as I can throw them... and I encourage you to do the same.

This guidelines originates from an issue with ABI compatibility: the ABI is a complex set of specifications that defines the exact interface of a compiled library. It is includes notably:

  • the memory layout of structures
  • the name mangling of functions
  • the calling conventions of functions
  • the handling of exception, runtime type information, ...
  • ...

For more details, check for example the Itanium ABI. Contrary to C which has a very simple ABI, C++ has a much more complicated surface area... and therefore many different ABIs were created for it.

On top of ABI compatibility, there is also an issue with Standard Library Implementation. Most compilers come with their own implementation of the Standard Library, and these implementations are incompatible with each others (they do not, for example, represent a std::vector the same way, even though all implement the same interface and guarantees).

As a result, a compiled binary (executable or library) may only be mixed and matched with another compiled binary if both were compiled against the same ABI and with compatible versions of a Standard Library implementation.

Cheers: no issue if you distribute source code and let the client compile.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • Can you comment on this related [Q&A](https://stackoverflow.com/questions/49059675/transitioning-away-from-stdstring-stdostream-etc-in-a-librarys-public-ap)? – metal Mar 01 '18 at 23:45
1

If you are using C++11, you can use cppcomponents. https://github.com/jbandela/cppcomponents

This will allow you to use among other things std::vector as a parameter or return value across Dll/or .so files created using different compilers or standard libraries. Take a look at my answer to a similar question for an example Passing reference to STL vector over dll boundary

Note for the example, you need to add a CPPCOMPONENTS_REGISTER(ImplementFiles) after the CPPCOMPONENTS_DEFINE_FACTORY() statement

Community
  • 1
  • 1
John Bandela
  • 2,416
  • 12
  • 19