Каков наилучший способ написания общих шаблонных функций, где T может быть std :: array (fixed-size) или std :: vector / list / deque (динамически размещаемая) - PullRequest
0 голосов
/ 16 апреля 2020

Я хотел бы написать шаблонную функцию, которая выполняет математические вычисления для контейнеров (математических) векторов. Мне нужна гибкость в аргументе шаблона T, чтобы он мог быть std::array или std::vector или std::list. Эти типы имеют очень похожие интерфейсы, с основным отличием в том, что std::array является типом фиксированного размера, тогда как std::list и std::vector могут быть динамически изменены (хотя и с различными моделями памяти).

Для ради этого примера давайте предположим, что внешний контейнер всегда std::vector и что функция возвращает вновь созданный вектор Ts:

#include <vector>
#include <array>
#include <iostream>
using std::cout;
using std::endl;

template<typename T>
std::vector<T>
process_vectors (const std::vector<T>& vecs) {
    // Create the return object with the same numbers of T objects it is as vecs:
    std::vector<T> rtn (vecs.size());

    // The missing code, if T is std::vector, is to resize each member of rtn to
    // have the same size as each element of vecs. What's the best way to introduce
    // this small difference between the implementations?

    // Use iterators to work through vecs and rtn, as these are a common interface to
    // all the STL containers
    typename std::vector<T>::const_iterator vi = vecs.begin();
    typename std::vector<T>::iterator ri = rtn.begin();

    // For this example, let's just copy the input to the output, copying each T
    // element by element:
    while (vi != vecs.end()) {

        // For each mathematical vector in vecs, loop through its vector components:
        typename T::const_iterator vi_i = vi->begin();
        // And copy the result into the return's components:
        typename T::iterator ri_i = ri->begin();

        while (vi_i != vi->end()) {
            // The 'operation' of this function; performing a copy 
            // (My real code would do something more useful, like
            // auto-scaling the lengths of the vectors)
            *ri_i = *vi_i;
            // On to the next elements:
            ++vi_i;
            ++ri_i;
        }
        ++vi;
        ++ri;
    }

    return rtn;
}

int main() {
    // vector<array> version
    std::vector<std::array<float, 3>> the_vecs(3);
    the_vecs[0] = { 1.0f, 1.0f, 1.0f };
    the_vecs[1] = { 2.0f, 2.0f, 2.0f };
    the_vecs[2] = { 3.0f, 3.0f, 3.0f };
    std::vector<std::array<float, 3>> rtn_obj = process_vectors (the_vecs);
    cout << "vector<array> rtn_obj size is " << rtn_obj.size() << endl;

    // vector<vector> version will crash
    std::vector<std::vector<float>> the_vecs2;
    the_vecs2.push_back ({ 1.0f, 1.0f, 1.0f });
    the_vecs2.push_back ({ 2.0f, 2.0f, 2.0f });
    the_vecs2.push_back ({ 3.0f, 3.0f, 3.0f });
    std::vector<std::vector<float>> rtn_obj2 = process_vectors (the_vecs2);
    cout << "vector<vector> rtn_obj2 size is " << rtn_obj2.size() << endl;

    return 0;
}

Если вы скомпилируете и запустите этот пример, первая строфа в main() будет работать нормально, а второй (vector<vector> один) обработает sh с ошибкой памяти, поскольку элементы rtn не выделены. Итак, насколько я могу видеть, мне нужно создать две версии этой функции, одну с изменением размера для vector<list> или vector<vector> типизированных аргументов, и одну без изменения размера для аргументов, набранных как vector<array<float, 3>>.

Теперь я знаю, как тестировать во время компиляции другой тип T, чтобы я мог написать две отдельные реализации этой функции, в зависимости от того, T равен array или vector. Я использую этот код:

#include <array>
#include <list>
#include <type_traits>
#include <utility>
#include <vector>
#include <type_traits>
#include <iostream>
using std::cout;
using std::endl;

// specialize a type for resizable stl containers
namespace is_resizable_vector_impl {
    template <typename T>       struct is_resizable_vector:std::false_type{};
    template <typename... Args> struct is_resizable_vector<std::vector <Args...>>:std::true_type{};
    template <typename... Args> struct is_resizable_vector<std::list   <Args...>>:std::true_type{};
    // etc, other types omitted
}
// I've omitted a similar test for fixed-size stl containers (i.e. std::array)

// From the typename T, set a value attribute which says whether T is a scalar (like
// float, double), a resizable list-like type (std::vector, std::list etc) or
// a fixed-size list-like type, such as std::array.
template <typename T>
struct number_type {
    static constexpr bool const scalar = std::is_scalar<std::decay_t<T>>::value;
    static constexpr bool const resizable = is_resizable_vector_impl::is_resizable_vector<std::decay_t<T>>::value;
    // 0 default                                    value 0 for default impl (vector-common)
    // 1 scalar == false and resizable == true   => value 1 for resizable vector implementations
    // 2 scalar == false and resizable == false  => value 2 for fixed-size vector implementations
    // 3 scalar == true                          => value 3 for scalar
    static constexpr int const value = scalar ? 3 : (resizable ? 1 : 2);
};

// Common/default implementation
template <int vtype = 0>
struct Implementation
{
    template<typename T>
    static std::vector<T> process_vectors (const std::vector<T>& vecs) {
        std::vector<T> rtn (vecs.size());
        // common/default implementation if possible...
        return rtn;
    }
};

// Resizable (T is std::vector or std::list) implementation
template <>
struct Implementation<1>
{
    template<typename T>
    static std::vector<T> process_vectors (const std::vector<T>& vecs) {
        std::vector<T> rtn (vecs.size());
        cout << "resizable T implementation with .resize()s" << endl;
        return rtn;
    }
};

// Fixed-size (T is std::array) implementation
template <>
struct Implementation<2>
{
    template<typename T>
    static std::vector<T> process_vectors (const std::vector<T>& vecs) {
        std::vector<T> rtn (vecs.size());
        cout << "fixed-size T implementation WITHOUT .resize()s" << endl;
        return rtn;
    }
};

// Scalar implementation omitted; it's outside the scope of this stackoverflow question

// Now I can write out
template<typename T>
std::vector<T> process_vectors (const std::vector<T>& vecs) {
    return Implementation<number_type<T>::value>::process_vectors (vecs);
}

int main ()
{
    // vector<array> version
    std::vector<std::array<float, 3>> the_vecs(3);
    the_vecs[0] = { 1.0f, 1.0f, 1.0f };
    the_vecs[1] = { 2.0f, 2.0f, 2.0f };
    the_vecs[2] = { 3.0f, 3.0f, 3.0f };
    std::vector<std::array<float, 3>> rtn_obj = process_vectors (the_vecs);

    // vector<vector> version
    std::vector<std::vector<float>> the_vecs2;
    the_vecs2.push_back ({ 1.0f, 1.0f, 1.0f });
    the_vecs2.push_back ({ 2.0f, 2.0f, 2.0f });
    the_vecs2.push_back ({ 3.0f, 3.0f, 3.0f });
    std::vector<std::vector<float>> rtn_obj2 = process_vectors (the_vecs2);

    return 0;
}

Проблема в том, что мне все еще приходится дублировать довольно много кода (с копией функции в struct Implementation<1> и другой в struct Implementation<2>), даже хотя единственная разница здесь заключается в необходимости изменения размера каждого элемента rtn. Итак, вопрос «как бы вы избежали дублирования функции process_vectors?»

Спасибо за чтение!

1 Ответ

0 голосов
/ 16 апреля 2020

Благодаря @NathanOliver, который указал, что объект vector<T> rtn можно инициализировать со всеми элементами, правильно выделенными с небольшим изменением: замените std::vector<T> rtn (vecs.size()); на std::vector<T> rtn (vecs);.

Таким образом, ответ на вопрос а) использовать итераторы для доступа к элементам std::vector или std::array объекта и b) использовать встроенный ум конструкторам контейнеров STL для обеспечения правильного распределения памяти .

Код рабочего решения:

#include <vector>
#include <array>
#include <iostream>
using std::cout;
using std::endl;

template<typename T>
std::vector<T>
process_vectors (const std::vector<T>& vecs) {
    // Create the return object with the same number of T objects in it as there are
    // in vecs, and with each T correctly allocated:
    std::vector<T> rtn (vecs);

    // Use iterators to work through vecs and rtn, as these are a common interface to
    // all the STL containers
    typename std::vector<T>::const_iterator vi = vecs.begin();
    typename std::vector<T>::iterator ri = rtn.begin();

    // For this example, let's just copy the input to the output, copying each T
    // element by element:
    while (vi != vecs.end()) {

        // For each mathematical vector in vecs, loop through its vector components:
        typename T::const_iterator vi_i = vi->begin();
        // And copy the result into the return's components:
        typename T::iterator ri_i = ri->begin();

        while (vi_i != vi->end()) {
            // The trivial example 'operation' of this function; performing a copy:
            *ri_i = *vi_i;
            // On to the next elements:
            ++vi_i;
            ++ri_i;
        }
        ++vi;
        ++ri;
    }

    return rtn;
}

int main() {
    // vector<array> version
    std::vector<std::array<float, 3>> the_vecs(3);
    the_vecs[0] = { 1.0f, 1.0f, 1.0f };
    the_vecs[1] = { 2.0f, 2.0f, 2.0f };
    the_vecs[2] = { 3.0f, 3.0f, 3.0f };
    std::vector<std::array<float, 3>> rtn_obj = process_vectors (the_vecs);
    cout << "vector<array> rtn_obj size is " << rtn_obj.size() << endl;
    cout << "Middle vector in rtn_obj2 is (" << rtn_obj[1][0] << "," << rtn_obj[1][1] << ","  << rtn_obj[1][2] << ")\n";

    // vector<vector> version
    std::vector<std::vector<float>> the_vecs2;
    the_vecs2.push_back ({ 1.0f, 1.0f, 1.0f });
    the_vecs2.push_back ({ 2.0f, 2.0f, 2.0f });
    the_vecs2.push_back ({ 3.0f, 3.0f, 3.0f });
    std::vector<std::vector<float>> rtn_obj2 = process_vectors (the_vecs2);
    cout << "vector<vector> rtn_obj2 size is " << rtn_obj2.size() << endl;
    cout << "Middle vector in rtn_obj2 is (" << rtn_obj2[1][0] << "," << rtn_obj2[1][1] << ","  << rtn_obj2[1][2] << ")\n";

    return 0;
}

С выводом:

vector<array> rtn_obj size is 3
Middle vector in rtn_obj2 is (2,2,2)
vector<vector> rtn_obj2 size is 3
Middle vector in rtn_obj2 is (2,2,2)
...