Reputation: 187
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
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
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
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