JtTest
JtTest

Reputation: 187

From std::vector to pointer-to-array (most C++-style solution)

In the C++ application that I am writing, at some point, I have to interface with a C API and call a function whose signature is

int foo(const double(*bar)[3], const int N) 

My bar is a std::vector<double> whose size is a multiple of 3. This design is required by other parts of the application and cannot be changed. Therefore, to pass bar from my source code to foo, I arranged the following C-style solution:

auto bar = std::vector<double>(3 * N); // for some int N known only at runtime
// a lot of code that uses bar
// ...
// Call to C function "foo"
auto *bar_p = bar.data()
int result = foo((double(*)[3])(&bar_p), N)

which compiled but made clang-tidy to complain about using C-style arrays. I also tried with some static_cast solutions, but the compiler did not accept them. Furthermore, clang-tidy also complained when I tried to use reinterpret_cast.

Which is the supposed/canonical/best/"right" C++-way to pass bar to foo?

P.S.: C++17 or higher

Upvotes: 4

Views: 169

Answers (3)

0___________
0___________

Reputation: 67835

To be portable and 100% UB free you need to create a new object and copy data to it

#include <vector>
#include <cstddef> // For std::size_t

template <typename T, std::size_t N>
T (*vector_to_array(const std::vector<T>& vec, std::size_t& out_rows))[N] 
{
    std::size_t total_elements = vec.size();
    std::size_t rows = (total_elements + N - 1) / N; 
    out_rows = rows;

    T (*array_ptr)[N] = new T[rows][N]; // Allocates 'rows' arrays of 'N' elements

    for (std::size_t i = 0; i < total_elements; ++i) 
    {
        array_ptr[i / N][i % N] = vec[i];
    }

    for (std::size_t i = total_elements; i < rows * N; ++i)  
    {
        array_ptr[i / N][i % N] = T(); 
    }

    return array_ptr;
}

or using smart pointers

template <typename T, std::size_t N>
std::shared_ptr<T[][N]> vector_to_array(const std::vector<T>& vec, std::size_t& out_rows)
{
    std::size_t total_elements = vec.size();
    std::size_t rows = (total_elements + N - 1) / N;
    out_rows = rows;
    T (*array_ptr)[N] = new T[rows][N];
    for (std::size_t i = 0; i < total_elements; ++i)
    {
        array_ptr[i / N][i % N] = vec[i];
    }
    for (std::size_t i = total_elements; i < rows * N; ++i)
    {
        array_ptr[i / N][i % N] = T();
    }
    auto deleter = [](T (*p)[N])
    {
        delete[] p;
    };
    return std::shared_ptr<T[][N]>(array_ptr, deleter);
}

The whole example:

#include <vector>
#include <memory>
#include <iostream>
#include <random>

template <typename T, std::size_t N>
std::shared_ptr<T[][N]> vector_to_array(const std::vector<T>& vec, std::size_t& out_rows)
{
    std::size_t total_elements = vec.size();
    std::size_t rows = (total_elements + N - 1) / N;
    out_rows = rows;
    T (*array_ptr)[N] = new T[rows][N];
    for (std::size_t i = 0; i < total_elements; ++i)
    {
        array_ptr[i / N][i % N] = vec[i];
    }
    for (std::size_t i = total_elements; i < rows * N; ++i)
    {
        array_ptr[i / N][i % N] = T();
    }
    auto deleter = [](T (*p)[N])
    {
        delete[] p;
    };
    return std::shared_ptr<T[][N]>(array_ptr, deleter);
}

int foo(const double (*bar)[3], const int N) 
{
    // Example implementation to demonstrate access
    for (int i = 0; i < N; ++i) 
    {
        std::cout << bar[i][0] << " " << bar[i][1] << " " << bar[i][2] << std::endl;
    }
    return 0; 
}

void callFoo(int n) 
{
    auto bar = std::vector<double>(3 * n);

    for (int i = 0; i < 3 * n; ++i) 
    {
        bar[i] = static_cast<double>(i);
    }

    std::size_t outr;
    auto bar_p = vector_to_array<double,3>(bar, outr);

    // Call foo
    int result = foo(bar_p.get(), n);
    std::cout << "result = " << result << " out_rows = " << outr << std::endl;
}

int main()
{
    std::random_device rd;  
    std::mt19937 gen(rd()); 

    std::uniform_int_distribution<> distrib(5, 10);
    callFoo(distrib(gen));
}

https://godbolt.org/z/MbGGe954j

added MSVC executor: https://godbolt.org/z/9jfoqMzK3

Upvotes: 0

Red.Wave
Red.Wave

Reputation: 4249

I am afraid there is no std portable solution to the problem. But it's at least possible to force compilation to fail if a supposed solution is not applicable. I am a proponent of always using std::array instead of raw C arrays. When interfaced with C API, we are usually certain that data types have standard layouts. So, we only need to assert that std::array can replace raw C arrays. So here's my first trail:

#include <span>
#include <array>
#include <type_traits>

typedef double raw_double_3[3];
using double_3 = std::array<std::remove_extent_t<raw_double_3> ,std::extent_v<raw_double_3>>;

auto cpp_foo(std::span<const double_3> val){ 
    static_assert(std::is_standard_layout_v<double_3>);
    static_assert(sizeof(double_3)==sizeof(raw_double_3));

    auto const &ref = reinterpret_cast<const raw_double_3&>(front(val));
    return foo(&ref, size(val));
};

This wrapper can handle std::vector<double_3> as well as std::array<double_3, N> or raw array of double_3 elements. As long as the assertions succeed(very [[likely]]), the compiled wrapper is UB-free. In the unlikely event that the assertions fail, we can look for other workarounds. In either case we should investigate provable preconditions for UB-free and declare static_assert on them. Thus, the code either runs bug-free, or fails to compile. Compiler/sanitizer warnings may still be present; because we are doing some unorthodox ad-hoc anyway. But it's readable and we have the safety proof (proper static_assert declarations), which also serve as minimal self documentation too.

Upvotes: 1

Ted Lyngmo
Ted Lyngmo

Reputation: 117812

Since you need to be able to provide a pointer to the first double[3] in a contiguous memory area, you will need to actually create double[3]s in that area. The simple solution is to use a std::vector<double[3]> or even a std::unique_ptr<double[3]> and suppress the clang-tidy warning. Provide a motivation, should you get negative code reviews:

// NOLINTNEXTLINE(modernize-avoid-c-arrays) C array used with C API
std::vector<double[3]> bar(N);
// ...
int result = foo(bar.data(), static_cast<int>(bar.size()));

or

// NOLINTNEXTLINE(modernize-avoid-c-arrays) C array used with C API
std::unique_ptr<double[3]> bar(new double[N][3]{});
int result = foo(bar.get(), N);

Upvotes: 3

Related Questions