最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c++ - Static (constexpr) function dispatch and dependency injection with templates in C++23 - Stack Overflow

programmeradmin0浏览0评论

I am writing some unit tests for a C++ project.

As part of this work, I want to write some unit tests which mock the behaviour of a socket object. More precicely, I am trying to write an test for a function which depends on recv. In other words, the function I wish to write a test for calls recv.

The typical way to write a test with a dependency such as this would be to find a runtime dispatch solution. There are at least two straightforward approaches.

  • Write an interface class with two derived implementation classes. One of these implementations is the "real" implementation. It wraps recv. The other is a "fake" or mocked implementation. Virtual function calls and dynamic dispatch at runtime provide the solution here.
  • Use a std::variant (or similar). In this case, a simple if statement can be used to switch between real and mock function calls at runtime.

My thoughts are that there may be a better solution which does not require runtime dispatch. It might be possible to use constexpr, or some other compile time solution, to switch between calling two implementations of a function.

This might have to be combined with dependency injection. In this case, the dependency may be a type T injected using a template parameter.

To make things more concrete, below is a sketch for an implementation of some existing code which has not yet had unit tests added to it.

ssize_t do_logic(
    const int sock_fd,
    const std::unique_ptr<uint8_t[]> &p_buffer,
    const size_t buffer_size
) {

    // imagine there is some code block here
    {
        // code block A
    }

    ssize_t recv_size = recv(sock_fd, p_buffer.get(), buffer_size, 0);

    // imaging there is some code block here
    {
        // code block B
    }

    return recv_size;
}

int main() {

    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);

    // this is all just MWE noise
    sockaddr_in server_address;
    std::memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(1234);
    bind(sock_fd, reinterpret_cast<sockaddr*>(&server_address), sizeof(server_address));
    listen(sock_fd, 10);

    const size_t buffer_size = 1024;
    const auto p_buffer = std::make_unique<uint8_t[]>(buffer_size);

    do_logic(sock_fd, p_buffer, buffer_size);

    return 0;
}

Things to note:

  • The function do_logic has a dependency on recv. (A function from sockets library.)
  • do_logic also has some additional code which I am trying to avoid repeating. (DRY) These are represented by the code blocks block A and block B.

Here is an example main function for a unit test.

int main() {

    std::string sock_data = "Hello World. This is some example data for unit testing."

    const size_t buffer_size = 1024;
    const auto p_buffer = std::make_unique<uint8_t[]>(buffer_size);

    do_logic(sock_data, p_buffer, buffer_size);

    return 0;
}

So far all is well. We can write an implementation of do_logic which uses function overloading. (Compile time.) However, this is where things go wrong.

ssize_t do_logic(
    std::string sock_data,
    const std::unique_ptr<uint8_t[]> &p_buffer,
    const size_t buffer_size
) {

    // imagine there is some code block here
    {
        // code block A
    }

    ssize_t recv_size = recv(sock_data, p_buffer.get(), buffer_size);

    // imaging there is some code block here
    {
        // code block B
    }

    return recv_size;
}

The problem here is a violation of DRY. While this will work, it is not ideal to have to maintain a synchronization of two code blocks across two different files in a code base.

In this case, block A and block B now existing in two different files. Those files are probably in very different places in the source tree, since one of these files is for unit testing, while the other is for production code.

As an asside, we can of course write an implementation of recv(std::string, ... etc ...). (Not shown here, but it would probably call std::copy, or something.)

Is there a solution to this problem which does not require resorting to runtime dispatch?

My initial thoughts were that perhaps a template parameter could be used to write an implementation of do_logic. This is a kind of dependency injection, where the type is injected into the function.

template<typename T>
ssize_t do_logic(
    T sock,
    const std::unique_ptr<uint8_t[]> &p_buffer,
    const size_t buffer_size
) {

    // imagine there is some code block here
    {
        // code block A
    }

    constexpr if ( /* ? */ ) {
        // this `recv`: the `recv` from the sockets library
        ssize_t recv_size = recv(sock, p_buffer.get(), buffer_size, 0);
    }
    else {
        // this `recv`: an implementation of a function `recv(std::string, ...etc...)`
        // which we write. It could be a null implementation, or it could call
        // `std::copy`.
        //
        // ssize_t recv(std::string, const auto& p_buffer, const auto buffer_size) { return 0; }
        //
        ssize_t recv_size = recv(sock, p_buffer.get(), buffer_size);
    }

    // imaging there is some code block here
    {
        // code block B
    }

    return recv_size;
}

For the line constexpr if ( /* ? */ ), what I want to do is something like

constexpr if ( T isa typename int ) { }
else if ( T isa typename std::string ) { }
else {
    static_assert<false>("T must be either type int or type std::string");
}

However, I have no idea how to implement that. I am vaguely aware that C++ 20 (?) introduced concepts. However, from my initial reading into concepts, my interpretation was that concepts are a more abstract idea which groups related types together. I'm not sure if it is the right tool for the job?

To be honest, there may be a simpler approach than the idea I have proposed here. It is quite likely that I am taking this in the wrong direction. Feedback on this would be much appreciated. Thank you in advance.

I am writing some unit tests for a C++ project.

As part of this work, I want to write some unit tests which mock the behaviour of a socket object. More precicely, I am trying to write an test for a function which depends on recv. In other words, the function I wish to write a test for calls recv.

The typical way to write a test with a dependency such as this would be to find a runtime dispatch solution. There are at least two straightforward approaches.

  • Write an interface class with two derived implementation classes. One of these implementations is the "real" implementation. It wraps recv. The other is a "fake" or mocked implementation. Virtual function calls and dynamic dispatch at runtime provide the solution here.
  • Use a std::variant (or similar). In this case, a simple if statement can be used to switch between real and mock function calls at runtime.

My thoughts are that there may be a better solution which does not require runtime dispatch. It might be possible to use constexpr, or some other compile time solution, to switch between calling two implementations of a function.

This might have to be combined with dependency injection. In this case, the dependency may be a type T injected using a template parameter.

To make things more concrete, below is a sketch for an implementation of some existing code which has not yet had unit tests added to it.

ssize_t do_logic(
    const int sock_fd,
    const std::unique_ptr<uint8_t[]> &p_buffer,
    const size_t buffer_size
) {

    // imagine there is some code block here
    {
        // code block A
    }

    ssize_t recv_size = recv(sock_fd, p_buffer.get(), buffer_size, 0);

    // imaging there is some code block here
    {
        // code block B
    }

    return recv_size;
}

int main() {

    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);

    // this is all just MWE noise
    sockaddr_in server_address;
    std::memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl(INADDR_ANY);
    server_address.sin_port = htons(1234);
    bind(sock_fd, reinterpret_cast<sockaddr*>(&server_address), sizeof(server_address));
    listen(sock_fd, 10);

    const size_t buffer_size = 1024;
    const auto p_buffer = std::make_unique<uint8_t[]>(buffer_size);

    do_logic(sock_fd, p_buffer, buffer_size);

    return 0;
}

Things to note:

  • The function do_logic has a dependency on recv. (A function from sockets library.)
  • do_logic also has some additional code which I am trying to avoid repeating. (DRY) These are represented by the code blocks block A and block B.

Here is an example main function for a unit test.

int main() {

    std::string sock_data = "Hello World. This is some example data for unit testing."

    const size_t buffer_size = 1024;
    const auto p_buffer = std::make_unique<uint8_t[]>(buffer_size);

    do_logic(sock_data, p_buffer, buffer_size);

    return 0;
}

So far all is well. We can write an implementation of do_logic which uses function overloading. (Compile time.) However, this is where things go wrong.

ssize_t do_logic(
    std::string sock_data,
    const std::unique_ptr<uint8_t[]> &p_buffer,
    const size_t buffer_size
) {

    // imagine there is some code block here
    {
        // code block A
    }

    ssize_t recv_size = recv(sock_data, p_buffer.get(), buffer_size);

    // imaging there is some code block here
    {
        // code block B
    }

    return recv_size;
}

The problem here is a violation of DRY. While this will work, it is not ideal to have to maintain a synchronization of two code blocks across two different files in a code base.

In this case, block A and block B now existing in two different files. Those files are probably in very different places in the source tree, since one of these files is for unit testing, while the other is for production code.

As an asside, we can of course write an implementation of recv(std::string, ... etc ...). (Not shown here, but it would probably call std::copy, or something.)

Is there a solution to this problem which does not require resorting to runtime dispatch?

My initial thoughts were that perhaps a template parameter could be used to write an implementation of do_logic. This is a kind of dependency injection, where the type is injected into the function.

template<typename T>
ssize_t do_logic(
    T sock,
    const std::unique_ptr<uint8_t[]> &p_buffer,
    const size_t buffer_size
) {

    // imagine there is some code block here
    {
        // code block A
    }

    constexpr if ( /* ? */ ) {
        // this `recv`: the `recv` from the sockets library
        ssize_t recv_size = recv(sock, p_buffer.get(), buffer_size, 0);
    }
    else {
        // this `recv`: an implementation of a function `recv(std::string, ...etc...)`
        // which we write. It could be a null implementation, or it could call
        // `std::copy`.
        //
        // ssize_t recv(std::string, const auto& p_buffer, const auto buffer_size) { return 0; }
        //
        ssize_t recv_size = recv(sock, p_buffer.get(), buffer_size);
    }

    // imaging there is some code block here
    {
        // code block B
    }

    return recv_size;
}

For the line constexpr if ( /* ? */ ), what I want to do is something like

constexpr if ( T isa typename int ) { }
else if ( T isa typename std::string ) { }
else {
    static_assert<false>("T must be either type int or type std::string");
}

However, I have no idea how to implement that. I am vaguely aware that C++ 20 (?) introduced concepts. However, from my initial reading into concepts, my interpretation was that concepts are a more abstract idea which groups related types together. I'm not sure if it is the right tool for the job?

To be honest, there may be a simpler approach than the idea I have proposed here. It is quite likely that I am taking this in the wrong direction. Feedback on this would be much appreciated. Thank you in advance.

Share Improve this question edited Mar 2 at 13:51 康桓瑋 43.6k5 gold badges63 silver badges127 bronze badges asked Mar 2 at 13:32 user2138149user2138149 17.7k30 gold badges150 silver badges296 bronze badges 3
  • You are probably looking for if constexpr (std::is_same_v<T, std::string>) { ... }. That said, if you already have an overload recv(std::string, ...), then you don't need any if constexpr. Just call recv(sock, ...) and the overload resolution would pick the right function. – Igor Tandetnik Commented Mar 2 at 15:00
  • 1 In my experience, I'd consider std::ostream as a poster child example. If the function takes void do_something(std::ostream& out) { /*...*/ } you can pass in your std::ofstream for the real code, and std::ostringstram as your mock argument in your unit tests. This is how you get polymorphic reuse and adhere to open/close principle for do_something: it can work with future ostream objects that didn't exist when the function was written. It is decoupled from the concrete ostream. – Eljay Commented Mar 2 at 15:17
  • [OT] Passing unique_ptr by const reference seems strange, you might pass std::span<uint8_t> instead. – Jarod42 Commented Mar 3 at 8:57
Add a comment  | 

1 Answer 1

Reset to default 1

The problem here is a violation of DRY.

Create sub-functions then:

void blockA(/*..*/)
{
    // imagine there is some code block here
    {
        // code block A
    }
}
void blockB(/*..*/)
{
    // imagine there is some code block here
    {
        // code block B
    }
}

then

ssize_t do_logic(
    const int sock_fd,
    const std::unique_ptr<uint8_t[]> &p_buffer,
    const size_t buffer_size
) {
    blockA(/*...*/);
    ssize_t recv_size = recv(sock_fd, p_buffer.get(), buffer_size, 0);
    blockB(/*...*/);
    return recv_size;
}
ssize_t do_logic(
    std::string sock_data,
    const std::unique_ptr<uint8_t[]> &p_buffer,
    const size_t buffer_size
) {
    blockA(/*...*/);
    ssize_t recv_size = recv(sock_data, p_buffer.get(), buffer_size);
    blockB(/*...*/);
    return recv_size;
}

The typical way to write a test with a dependency such as this would be to find a runtime dispatch solution. There are at least two straightforward approaches.

Another erase-type useful in that regard is std::function

For compile type, template is a way to go:

// Mockable method
template <typename RECV_FUNC, typename SOCK>
ssize_t do_logic(
    RECV_FUNC f, // std::function<ssize_t (SOCK, uint8_t*, size_t)> to be runtime
    SOCK sock_data,
    const std::unique_ptr<uint8_t[]> &p_buffer,
    const size_t buffer_size
) {
    blockA(/*...*/);
    ssize_t recv_size = f(sock_data, p_buffer.get(), buffer_size);
    blockB(/*...*/);
    return recv_size;
}

// Regular entry
ssize_t do_logic(
    const int sock_fd,
    const std::unique_ptr<uint8_t[]> &p_buffer,
    const size_t buffer_size
) {
    return do_logic([](auto... args){ return recv(args...); },
                    sock_fd, p_buffer, buffer_size);
}

// Simple mocked entry (currently noop)
ssize_t do_logic(
    std::string sock_data,
    const std::unique_ptr<uint8_t[]> &p_buffer,
    const size_t buffer_size
) {
    return do_logic([](auto&... args){ /*..*/return 0; },
                    sock_data, p_buffer, buffer_size);
}
发布评论

评论列表(0)

  1. 暂无评论