0

This might be an opinionated question but I'm eager to hear your anecdotes as I'm at the beginning of designing a new API. Now I'm questioning whether I want this API to be a sink API or non-sink API. I call it that for the lack of better words (and I heard that somewhere so it might even be the right wording).

  • A sink API gobbles up all the resources it needs to operate, and allocates new memory for it if necessary.
  • A non-sink API just references into already existing datastructures and operates on them.

The easiest example is the one of string vs. string_view:

Demo

#include <string>
#include <string_view>

#include <cstdio>

class non_sink
{
public:
    non_sink(std::string_view name)
        :   name_{ name }
    {}
    
    std::string_view name_;
};

class sink
{
public:
    sink(std::string name)
        :   name_{ name }
    {}
    
    std::string name_;
};

int main()
{
    non_sink example1("mychannel");
    sink example2("mychannel");
}

The question that has been burning in my mind for long is what is better practice? Do I make my API's sink or non-sink or does it depend on the circumstances? If so, is there a commen denominator for advertising the "sink'ness" of an API so the user can understand it? What is your take?

1
  • They aren't better or worse, they do different things. A non sink api that holds onto a resource is incorrect, because of lifetime issues.
    – Caleth
    Mar 13, 2023 at 10:16

1 Answer 1

7

It might be clearer to think about owned vs borrowed data.

Owned data duplicates data in memory, but is much easier to use correctly. With owned data, it is less likely to encounter object lifetime problems such as use-after-frees. It is also less likely to encounter aliasing problems, where the same data is used in multiple contexts, and one context sees unexpected changes from another context.

Borrowed data is of course more efficient, but suffers from the aforementioned problems. Other languages have facilities to use shared or borrowed data more safely (Haskell: immutability, Rust: lifetime annotations, Java: garbage collection), but C++ does not. The programmer is responsible for making sure that everything will work out correctly.

Your non_sink class represents borrowed data. However, its name doesn't make clear that such borrowing is occurring, so it is likely that developers might use that class incorrectly, introducing bugs.

In general, the following decision scheme works:

  • By default, do the simplest, most robust thing that works. Freely make copies of data to prevent unexpected interactions.

  • If necessary, use safe abstractions such as smart pointers.

  • Use borrowed data only in function arguments. For example, std::string_view is a fine replacement for a const std::string& function argument. The point here is that the C++ language guarantees that function arguments will live as long as the function is executing, so borrowing arguments is typically safe (if the borrowed data doesn't escape the function).

  • Use borrowed data in other contexts only if you have stringent performance requirements. Borrowed data in local variables, global variables, or in struct members is typically a red flag. There are uses for this, but it's more difficult to get right. That you have a struct with a std::string_view member could be a violation of this.

1
  • Thank you for your reflected answer. That reinforces the doubts that I had about borrowing data. I notice a patern emerging in the C++ community that API's that operate on borrowed data are postfixed with "_view" such as the afforementioned string_view but also ranges::view and alike.
    – glades
    Mar 13, 2023 at 10:26

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.