Some questions for improvements on template / meta programming techniques.
Motivation for the code below is conveniently extracting correctly typed data from an sql db driver with a c-style API. Rows of the result set are returned one by one as char**. I simulate that with some array literals below.
I wrap this char** row data in a class to provide type extraction. Also there is a templated single_column() function which can return a singly typed column in a container of the caller's choice.
The code works as shown. Questions are:
- blocking the non-specialised version of
row::get<ValueType>with astatic_cast<void>seems like a hack. Is there a better way? I don't think the answer is to simply writeget_int(),get_long()etc, because we want generic code likesingle_column()to just select the specified getter. - the type-trait "method exists dispatching" to detect presence of
push_back()withif constexprworks fine. Is there a more elegant way using C++17? ie without c++20 concepts?
Many thanks
#include <cstddef>
#include <iostream>
#include <string>
#include <unordered_set>
#include <vector>
// is there a more direct way (without using c++20 concepts)
template <typename C, typename = void>
struct has_push_back : std::false_type {};
template <typename C>
struct has_push_back<
C, std::void_t<decltype(std::declval<C>().push_back(std::declval<typename C::value_type>()))>>
: std::true_type {};
class row {
public:
explicit row(const char** row) : row_(row) {}
template <typename ValueType>
ValueType get(unsigned idx) const {
// should always fail! user must call an existing specialisation!
// is there a "better" way?
return static_cast<void>(row_[idx]); // NOLINT ptr arith
}
template <>
[[nodiscard]] int get<int>(unsigned idx) const {
return std::stoi(row_[idx]); // NOLINT ptr arith
}
template <>
[[nodiscard]] long get<long>(unsigned idx) const {
return std::stol(row_[idx]); // NOLINT ptr arith
}
template <>
[[nodiscard]] std::string get<std::string>(unsigned idx) const {
return row_[idx]; // NOLINT ptr arith
}
template <>
[[nodiscard]] const char* get<const char*>(unsigned idx) const {
return row_[idx]; // NOLINT ptr arith
}
private:
const char** row_;
};
template <typename ContainerType>
ContainerType single_column(const std::vector<row>& rows, unsigned col = 0) {
ContainerType values;
using ValueType = typename ContainerType::value_type;
for (auto&& row: rows) {
if constexpr (has_push_back<ContainerType>::value) // is this "best" way?
values.push_back(row.get<ValueType>(col));
else
values.insert(row.get<ValueType>(col));
}
return values;
}
int main() {
// dealing with this as c-arrays because that is how it comes from db driver
// represents raw data coming back from db
// NOLINTNEXTLINE C-arrays
const char* data[][3] = {
{
"1-some text",
"122222222",
"1333333333333333",
},
{
"2-some text",
"222222222",
"2333333333333333",
},
{
"2-some text", // dupe value
"322222222",
"3333333333333333",
},
};
std::vector<row> rows; // simulationg rows being returned the db server
for (auto& r: data) rows.emplace_back(r);
// now retrieve column-wise as correct types into vectors
for (auto&& e: single_column<std::vector<std::string>>(rows, 0)) std::cout << e << ",";
std::cout << std::endl;
for (auto&& e: single_column<std::vector<int>>(rows, 1)) std::cout << e << ",";
std::cout << std::endl;
for (auto&& e: single_column<std::vector<long>>(rows, 2)) std::cout << e << ",";
std::cout << std::endl;
// now into unordered_sets
for (auto&& e: single_column<std::unordered_set<std::string>>(rows, 0)) std::cout << e << ",";
std::cout << std::endl;
for (auto&& e: single_column<std::unordered_set<int>>(rows, 1)) std::cout << e << ",";
std::cout << std::endl;
for (auto&& e: single_column<std::unordered_set<long>>(rows, 2)) std::cout << e << ",";
std::cout << std::endl;
// compile error - the static_cast<void> fails: works, but is there a "better" way
// double d = rows[0].get<double>(1);
}