1

Note: Please take a moment and read the entire question if you are tempted to mark this as a duplicate. I have myself referenced the other related questions below but those questions are very much at the C++ language level. This question is at the C++ language level too but this question is also about API development for APIs involving templates that other questions do not seem to address.

I am exposing an API that to my users (client code). My API relies a lot on templates.

What does not work

Here is my API code:

// api.h
#include <vector>

template<typename T> void api_func(std::vector<T> v);
// api.cpp
#include <iostream>
#include <vector>

template<typename T> void api_func(std::vector<T> v)
{
    // This prints just the size, but in the actual API, we would be
    // doing more complex things.
    std::cout << v.size() << '\n';
}

Here is a possible client code:

// client.cpp
#include "api.h"

int main()
{
    std::vector<int> v {1, 2, 3, 4, 5};

    // Although this client is calling the API with a std::vector<int>
    // another client may call the API with another vector type such as
    // std::vector<std::string>.
    api_func(v);
}

Of course, this does not compile.

$ clang++ -std=c++11 api.cpp client.cpp 
Undefined symbols for architecture x86_64:
  "void api_func<int>(std::__1::vector<int, std::__1::allocator<int> >)", referenced from:
      _main in client-2a152c.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

What works

This issue has been discussed extensively on Stack Overflow already in the following posts:

According to those discussions, one way to fix the issue is by moving the template definition to header file too.

// api.h - fixed
#include <iostream>
#include <vector>

template<typename T> void api_func(std::vector<T> v)
{
    // This prints just the size, but in the actual API, we would be
    // doing more complex things.
    std::cout << v.size() << '\n';
}
# api.cpp is unnecessary now
rm api.cpp
# client.cpp remains the same

This does compile now.

$ clang++ -std=c++11 client.cpp && ./a.out 
5

Questions

The resolution shows that if I don't want to commit to specific types for the template parameters in my API (to ensure that the client has the flexibility to choose types appropriate to their needs), then any functions relying on template parameters need to be defined in the header file itself.

But when I do this for all functions in my API, it appears that all my business logic has moved to the header file. So my entire API is now a giant collection of .H files with absolutely no .CPP files.

My questions:

  • Am I doing something wrong that has caused my entire API implementation to be composed only of .H files and no .CPP files?
  • How are APIs like these generally designed? Are there any other techniques to bring back implementation code into .CPP files such that only minimum possible stuff goes into .H files?
Lone Learner
  • 18,088
  • 20
  • 102
  • 200
  • Boost is a primarily header only library - and I don't think there's anything wrong with that (there's many other examples of header only libraries as well) – UnholySheep May 18 '18 at 18:44
  • I was not sure whether to upvote for interesting issue, or downvote for leaving out the particulars needed to address the concrete instance of that issue. Ended up downvoting. – Cheers and hth. - Alf May 18 '18 at 18:58
  • @Cheersandhth.-Alf What kind of particulars would you like me to include in my question? If you mean, I should include the details of the exact API I am developing, wouldn't that be inappropriate for Stack Overflow? The exact API details are hundreds of lines of code, so I thought I should provide a minimal toy example here to demonstrate my problem. What do you think? – Lone Learner May 19 '18 at 03:40
  • Well SO is not really conducive to this kind of questioning, where getting down to the particulars would involve some back-and-forth communication. But you could create a minimalistic example that demonstrated at least some of the data flows and control flows of your API. Without that all you get is the general advice "type erasure". – Cheers and hth. - Alf May 19 '18 at 05:13

1 Answers1

1

You have dropped from your question the part that determines how to solve it. The body of your templates matter to determine how exactly to hide them (or if it is possible). But I will answer with what you gave me.

Your problem is basically the problem of type erasure.

Start with this:

template<typename T> void api_func(std::vector<T> v);

this erases no information about the type v.

But what are we using:

template<typename T> void api_func(std::vector<T> v) {
  // This prints just the size, but in the actual API, we would be
  // doing more complex things.
  std::cout << v.size() << '\n';
}

here, we are extracting the .size() from the vector, then manipulating that.

The fact we are then streaming it to cout is not a fact about the type being passed in; the fact we expect it to have a .size() is.

struct has_dot_size_ref {
  template<class T,
    std::enable_if_t< !std::is_same<T, has_dot_size_ref>{}, bool> = true
  >
  has_dot_size_ref( T const& t ):
    ptr( std::addressof(t) ),
    call_dot_size([](void const* ptr)->std::size_t{
      return static_cast<T const*>(ptr)->size();
    })
  {}
  std::size_t size() const {
    return call_dot_size(ptr);
  }
private:
  void const* ptr = 0;
  std::size_t(*call_dot_size)(void const*) = 0;
};

this is a type eraser. It takes its argument, and erases everything about it except the fact that the object has a .size() member that returns std::size_t.

Now in the header:

void api_func(has_dot_size_ref v);

and in the cpp file:

void api_func(has_dot_size_ref v) {
  // This prints just the size, but in the actual API, we would be
  // doing more complex things.
  std::cout << v.size() << '\n';
}

and now the code works.

If you are doing something more complicated, work out what exactly you need from the type in question, erase down to exactly those properties.

Some of your code remains in header files, others does not.

Not every problem can be type erased this way; sometimes the type erasure is the entire method. And these techniques, because they are manual, take a bit of work. In addition, the type erasure barrier can be expensive (to that end, sometimes you type erase range operations instead of element operations; or even synthesize the type erased range operation from a written element operation).

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • "You have dropped from your question the part that determines how to solve it." - What kind of additional information should I have included in my question? I ask because I see a similar comment made to my question, so I am curious to know what I am missing in the question. – Lone Learner May 19 '18 at 03:41
  • "work out what exactly you need from the type in question" - Let us say I am invoking all member functions of `std::vector` in my API implementation. Does this alter your answer? – Lone Learner May 19 '18 at 03:45
  • @lone You tried to simplify by removing what the function does. But what the function does determines how you solve this problem. Ivtype erased callong size; you can type erase callimg another method on the elements of a container. But if the library just does a foreach then a call on each element, it isn't much of a library and after erasing in header there is nothing left. On other cases you can erase a small detail and lots of complexity remains for the implementation. Again, details you consider unimportant matter here. – Yakk - Adam Nevraumont May 19 '18 at 11:09