Как лучше всего реализовать единственную функцию, которая принимает два аргумента std::array<int, [size]>, каждый с размером сдержанный на соответствующий набор значений, известных во время компиляции?
Вопрос «Передача в функцию std :: array неизвестного размера», хотя и похож, не имеет прямого отношения.
Следующее работает в C++ 14, но кажется излишне избыточным и беспорядочным:
#include <type_traits>
#include <array>
// Add legal/allowable sizes for std::array<> "types" here
// Note: Not married to this; perhaps preprocessor instead?
enum class SizesForArrayX : size_t { Three = 3, Four, Forty = 40 };
enum class SizesForArrayY : size_t { Two = 2, Three, EleventyTwelve = 122 };
// Messy, compile-time, value getter for the above enum classes
template <typename S>
constexpr size_t GetSizeValue(const S size)
{ return static_cast<std::underlying_type_t<S>>(size); }
// An example of the function in question; is Template Argument Deduction
// possible here?
// Note: only arrays of "legal"/"allowable" sizes should be passable
template <SizesForArrayX SX, SizesForArrayY SY>
void PickyArrayHandler(
const std::array<int, GetSizeValue(SX)>& x,
const std::array<int, GetSizeValue(SY)>& y)
{
// Do whatever
for (auto& i : x) i = 42;
for (auto& i : y) while (i --> -41) i = i;
}
Вызов вышеуказанного:
int main()
{
// Declare & (value-)initialize some arrays
std::array<int, GetSizeValue(SizesForArrayX::Forty)> x{};
std::array<int, GetSizeValue(SizesForArrayY::Two>) y{};
//PickyArrayHandler(x, y); // <- Doesn't work; C2672, C2783
// This works & handles arrays of any "allowable" size but the required
// template params are repetitions of the array declarations; ick
PickyArrayHandler<SizesForArrayX::Forty, SizesForArrayY::Two>(x, y);
}
... что некрасиво, неэлегантно, медленно компилируется и требует, чтобы заявленный размер массива соответствовал явному переданному "размеру" для шаблона функции PickyArrayHandler.
Для конкретного примера выше: Есть ли способ для шаблона PickyArrayHandler определить размеры переданных массивов?
Вообще говоря: Есть ли другой, лучший подход?
static_assert внутри функции.
@skeller без строго типизированного перечисления, как ограничить размеры массива?
@ Анри, спасибо. Я не понимаю, как это отразится на 2-м и 3-м пунктах
@quasinormalized Это невозможно с перечислением. Вы не можете перебирать непрерывное перечисление.
Отредактирован заголовок, чтобы удалить вводящее в заблуждение "перечисление"





немного покрутил и получил эту уменьшенную работу: может быть, это помогает:
enum SizesForArrayX : size_t { Three = 3, Four, Forty = 40 };
enum SizesForArrayY : size_t { Two = 2, EleventyTwelve = 122 };
template <size_t TX, size_t TY>
void PickyArrayHandler(
const std::array<int, TX>& x,
const std::array<int, TY>& y)
{
// Do whatever
}
int main()
{
// Declare & (value-)initialize some arrays
std::array<int, SizesForArrayX::Forty> x{};
std::array<int, SizesForArrayY::Two> y{};
PickyArrayHandler(x, y);
return 0;
}
Но тогда вы также можете сделать это с массивом любого размера; он ничего не ограничивает.
К сожалению, ваши перечисления не являются непрерывными, поэтому вы не можете просто перебирать перечисление, и вам нужно обрабатывать все случаи индивидуально. Поскольку размеры известны во время компиляции, вы можете использовать static_assert.
#include <array>
enum SizesForArrayX : size_t { Three = 3, Four, Forty = 40 };
enum SizesForArrayY : size_t { Two = 2, EleventyTwelve = 122 };
template <size_t TX, size_t TY>
void PickyArrayHandler(const std::array<int, TX> &x,
const std::array<int, TY> &y)
{
static_assert(TX == Three || TX == Four || TX == Forty,
"Size mismatch for x");
static_assert(TY == Two || TY == EleventyTwelve, "Size mismatch for y");
// Do whatever
}
int main()
{
// Declare & (value-)initialize some arrays
std::array<int, SizesForArrayX::Forty> x{};
std::array<int, SizesForArrayY::Two> y{};
PickyArrayHandler(x, y);
PickyArrayHandler(std::array<int, 4>{}, std::array<int, 2>{});
//PickyArrayHandler(std::array<int, 1>{}, std::array<int, 5>{}); // BOOM!
}
Лично я бы просто вручную ввел допустимые размеры в static_assert внутри PickyArrayHandler. Если это не вариант, потому что размеры будут использоваться в других частях вашей программы, а вы придерживаетесь принципа DRY, я бы использовал препроцессор.
#define FOREACH_ALLOWABLE_X(SEP_MACRO) \
SEP_MACRO(3) \
SEP_MACRO(4) \
SEP_MACRO(40) \
#define FOREACH_ALLOWABLE_Y(SEP_MACRO) \
SEP_MACRO(2) \
SEP_MACRO(3) \
SEP_MACRO(122) \
#define COMMA_SEP(NUM) NUM,
#define LOGIC_OR_SEP_X(NUM) N1 == NUM ||
#define LOGIC_OR_SEP_Y(NUM) N2 == NUM ||
#define END_LOGIC_OR false
// some arrays with your sizes incase you want to do runtime checking
namespace allowable_sizes
{
size_t x[] {FOREACH_ALLOWABLE_X(COMMA_SEP)};
size_t y[] {FOREACH_ALLOWABLE_Y(COMMA_SEP)};
}
template <size_t N1, size_t N2>
void PickyArrayHandler(const std::array<int, N1>& x, const std::array<int, N2>& y)
{
static_assert(FOREACH_ALLOWABLE_X(LOGIC_OR_SEP_X) END_LOGIC_OR);
static_assert(FOREACH_ALLOWABLE_Y(LOGIC_OR_SEP_Y) END_LOGIC_OR);
// do whatever
}
#undef FOREACH_ALLOWABLE_X
#undef FOREACH_ALLOWABLE_Y
#undef COMMA_SEP
#undef LOGIC_OR_SEP_X
#undef LOGIC_OR_SEP_Y
#undef END_LOGIC_OR
Некоторые пуристы C++ будут недовольны этим, но он выполняет свою работу.
Поскольку вы не кажетесь разборчивым в том, как определяются допустимые размеры, вы можете использовать признаки типа
#include <array>
template <size_t N> struct valid_size1 { enum { value = false }; };
template <size_t N> struct valid_size2 { enum { value = false }; };
template <> struct valid_size1<3> { enum { value = true }; };
template <> struct valid_size1<4> { enum { value = true }; };
template <> struct valid_size1<40> { enum { value = true }; };
template <> struct valid_size2<2> { enum { value = true }; };
template <> struct valid_size2<122> { enum { value = true }; };
template <size_t TX, size_t TY>
void PickyArrayHandler(const std::array<int, TX> &x,
const std::array<int, TY> &y)
{
static_assert(valid_size1<TX>::value, "Size 1 is invalid");
static_assert(valid_size2<TY>::value, "Size 2 is invalid");
// Do whatever
}
int main()
{
// Declare & (value-)initialize some arrays
std::array<int, 40> x{};
std::array<int, 2> y{};
PickyArrayHandler(x, y);
PickyArrayHandler(std::array<int, 4>{}, std::array<int, 2>{});
// PickyArrayHandler(std::array<int, 1>{}, std::array<int, 5>{}); // BOOM!
}
Вот решение с использованием массива:
#include <iostream>
#include <array>
constexpr size_t valid_1[] = { 3, 4, 40 };
constexpr size_t valid_2[] = { 2, 122 };
template <size_t V, size_t I=0>
struct is_valid1 { static constexpr bool value = V==valid_1[I] || is_valid1<V,I+1>::value; };
template <size_t V, size_t I=0>
struct is_valid2 { static constexpr bool value = V==valid_2[I] || is_valid2<V,I+1>::value; };
template <size_t V>
struct is_valid1<V, sizeof(valid_1)/sizeof(valid_1[0])>
{static constexpr bool value = false; };
template <size_t V>
struct is_valid2<V, sizeof(valid_2)/sizeof(valid_2[0])>
{static constexpr bool value = false; };
template <size_t TX, size_t TY>
void PickyArrayHandler(const std::array<int, TX> &x,
const std::array<int, TY> &y)
{
static_assert(is_valid1<TX>::value, "Size 1 is invalid");
static_assert(is_valid2<TY>::value, "Size 2 is invalid");
// Do whatever
}
Это работает, хотя и с предупреждениями о локальных var для isValid, которые не указаны. Не могли бы вы более подробно объяснить, что происходит в { typename valid_size1<TX>::type isValid; }, так как вы никогда не баловались типичными характеристиками?
@quasinormalized Чтобы создать экземпляр шаблона, он должен иметь возможность создать переменную типа valid_size1 <TX> :: type. Если тип valid_size1 <TX> :: type существует, он завершается успешно. В противном случае это не так. Переменные не используются, поэтому вы получите предупреждение.
это элегантно, мне это нравится :)
@agbinfo Круто, спасибо! В настоящее время я играю с гибридом этого ответа и Стивена, чтобы посмотреть, могу ли я устранить предупреждения компилятора. Есть ли у вас какие-нибудь идеи?
Обновлено для удаления предупреждений
Хороший. Хотя я думаю, что вы могли пропустить enum { value = false }; внутри скобок исходного шаблона структуры?
Другой вопрос: это просто соглашение о явном шаблоне неиспользуемого N? template <size_t /*N*/> struct valid_size1 { enum { value = false }; }; вроде работает нормально. Как называется этот синтаксис, чтобы я мог исследовать?
value = false не нужен, но не повредит - возможно, вы получите лучшую диагностику, но я не проверял. Если он опущен, отсутствие идентификатора value приведет к сбою шаблона. Что касается неиспользованного N, то мне известно не о каком-либо соглашении.
@agbinfo Без value = false компиляция прерывается с 'необъявленный идентификатор' в шаблоне вместо сбоя в static_assert(), как предполагалось. Если вы не возражаете против этого небольшого обновления, я бы принял этот ответ как полный. :)
Обновлено с помощью value = false в шаблоне по умолчанию
Я добавил решение с массивом в стиле C. Думаю, так лучше.
На мой взгляд, лучший способ решить эту проблему - написать свойство настраиваемого типа:
template <std::underlying_type_t<SizesForArrayX> SX>
struct is_size_x {
static constexpr bool value = false;
};
template <>
struct is_size_x<static_cast<std::underlying_type_t<SizesForArrayX>>(SizesForArrayX::Forty)>{
static constexpr bool value = true;
};
Я бы поместил их прямо под декларациями enum class, чтобы было легко проверить, все ли они у вас есть. Кто-нибудь более умный, чем я, вероятно, смог бы найти способ даже сделать это с вариативными template, так что вам понадобится только одна специализация.
Это утомительно, но если у вас небольшой набор значений, он должен быть достаточно быстрым и легким для использования в модульных тестах. Другой приятный момент в этом подходе заключается в том, что если у вас есть несколько функций, которым требуется один из этих специальных размеров, вам не нужно копировать / вставлять static_assert.
С чертами типа ваша функция становится тривиальной:
template <std::size_t SX, std::size_t SY>
void PickyArrayHandler(
std::array<int, SX>& x,
std::array<int, SY>& y)
{
static_assert(is_size_x<SX>::value, "Invalid size SX");
static_assert(is_size_y<SY>::value, "Invalid size SY");
// Do whatever
for (auto& i : x) i = 42;
for (auto& i : y) while (i --> -41) i = i;
}
Наконец, вы можете создать псевдоним типа, чтобы в первую очередь избежать создания недопустимых array:
template <typename T, SizesForArrayX SIZE>
using XArray =
std::array<T, static_cast<std::underlying_type_t<SizesForArrayX>>(SIZE)>;
template <typename T, SizesForArrayY SIZE>
using YArray =
std::array<T, static_cast<std::underlying_type_t<SizesForArrayY>>(SIZE)>;
Это помешает вам объявить array, если это не утвержденный размер:
XArray<int, SizesForArrayX::Forty> x{};
YArray<int, SizesForArrayY::Two> y{};
Этот отличный ответ заслуживает большего, чем то, что может предложить моя репутация ниже 15, которую может предложить невидимое голосование за. Любой подсказки по устранению лишних специализаций, пока я экспериментирую с гибридом этого ответа & @ agbinfo? Классы перечисления были просто моим первым ударом по проблеме и не должны сохраняться, пока размеры массива ограничены.
Я не знаю, как удалять специализации, поскольку это одна из уловок, лежащих в основе черт типа. Если у вас есть много значений, с которыми нужно иметь дело, я бы посоветовал написать короткий скрипт (подойдет python), который может генерировать соответствующий файл заголовка с enum и специализациями. Если я найду лучшее решение, я обновлю этот ответ.
У вас может быть шаблон типа is_of_size, который проверяет размер массива, а затем использует его для отключения шаблона, если один из размеров не совпадает, что-то вроде:
#include <array>
#include <type_traits>
// Forward template declaration without definition.
template <class T, T N, T... Sizes>
struct is_one_of;
// Specialization when there is a single value: Ends of the recursion,
// the size was not found, so we inherit from std::false_type.
template <class T, T N>
struct is_one_of<T, N>: public std::false_type {};
// Generic case definition: We inherit from std::integral_constant<bool, X>, where X
// is true if N == Size or if N is in Sizes... (via recursion).
template <class T, T N, T Size, T... Sizes>
struct is_one_of<T, N, Size, Sizes... >:
public std::integral_constant<
bool, N == Size || is_one_of<T, N, Sizes... >::value> {};
// Alias variable template, for simpler usage.
template <class T, T N, T... Sizes>
constexpr bool is_one_of_v = is_one_of<T, N, Sizes... >::value;
template <std::size_t N1, std::size_t N2,
std::enable_if_t<
(is_one_of_v<std::size_t, N1, 3, 4, 40>
&& is_one_of_v<std::size_t, N2, 2, 3, 122>), int> = 0>
void PickyArrayHandler(
const std::array<int, N1>& x,
const std::array<int, N2>& y)
{
}
Тогда вы можете просто:
PickyArrayHandler(std::array<int, 3>{}, std::array<int, 122>{}); // OK
PickyArrayHandler(std::array<int, 2>{}, std::array<int, 3>{}); // NOK
В C++ 17 вы могли бы (я думаю) заменить is_one_of на:
template <auto N, auto... Sizes>
struct is_one_of;
... и автоматически выводит std::size_t.
В C++ 20 вы можете использовать концепцию более четких сообщений об ошибках;)
Не уверен, что я достаточно знаком с шаблонами, чтобы следовать вашему коду; но +1 для NOK, который входит в мой лексикон сегодня и навсегда.
@quasinormalized Я добавил некоторую информацию - в основном у вас есть рекурсия шаблонов, которая проверяет, является ли переданный вами первый размер (N) одним из других Sizes. Я сделал общий шаблон, поэтому есть T, но у вас может быть более простой шаблон только для std::size_t.
@quasinormalized Идея довольно проста для понимания, если вы знакомы с рекурсией. Базовый случай - когда Sizes... пуст (первая специализация), и в этом случае результат ложный (std::false_type), иначе вы проверяете, соответствует ли Size первого размера N, или вы рекурсивно создаете экземпляр шаблона с остальными размерами Sizes....
Использование static_assert для недопустимых размеров нет - хорошее решение, потому что оно плохо работает с SFINAE; то есть средства TMP, такие как std::is_invocable и идиома обнаружения, будут возвращать ложные срабатывания для вызовов, которые на самом деле всегда приводят к ошибке. Намного лучше использовать SFINAE для удаления недопустимых размеров из набора перегрузки, что приведет к чему-то вроде следующего:
template<std::size_t SX, std::size_t SY,
typename = std::enable_if_t<IsValidArrayXSize<SX>{} && IsValidArrayYSize<SY>{}>>
void PickyArrayHandler(std::array<int, SX> const& x, std::array<int, SY> const& y) {
// Do whatever
}
Сначала нам нужно объявить наши допустимые размеры; Я не вижу здесь каких-либо преимуществ более строгой типизации, поэтому для списка целых чисел во время компиляции std::integer_sequence отлично работает и очень легкий:
using SizesForArrayX = std::index_sequence<3, 4, 40>;
using SizesForArrayY = std::index_sequence<2, 3, 122>;
Теперь о свойствах IsValidArraySize ... Самый простой путь - использовать правила C++ 14 Relaxed-constexpr и выполнить простой линейный поиск:
#include <initializer_list>
namespace detail {
template<std::size_t... VSs>
constexpr bool idx_seq_contains(std::index_sequence<VSs...>, std::size_t const s) {
for (auto const vs : {VSs...}) {
if (vs == s) {
return true;
}
}
return false;
}
} // namespace detail
template<std::size_t S>
using IsValidArrayXSize
= std::integral_constant<bool, detail::idx_seq_contains(SizesForArrayX{}, S)>;
template<std::size_t S>
using IsValidArrayYSize
= std::integral_constant<bool, detail::idx_seq_contains(SizesForArrayY{}, S)>;
Однако, если время компиляции вообще вызывает беспокойство, я подозреваю, что следующее будет лучше, если потенциально менее ясно:
namespace detail {
template<bool... Bs>
using bool_sequence = std::integer_sequence<bool, Bs...>;
template<typename, std::size_t>
struct idx_seq_contains;
template<std::size_t... VSs, std::size_t S>
struct idx_seq_contains<std::index_sequence<VSs...>, S>
: std::integral_constant<bool, !std::is_same<bool_sequence<(VSs == S)...>,
bool_sequence<(VSs, false)...>>{}>
{ };
} // namespace detail
template<std::size_t S>
using IsValidArrayXSize = detail::idx_seq_contains<SizesForArrayX, S>;
template<std::size_t S>
using IsValidArrayYSize = detail::idx_seq_contains<SizesForArrayY, S>;
Какой бы маршрут реализации ни был выбран, использование SFINAE таким образом позволяет получать очень хорошие сообщения об ошибках - например, для PickyArrayHandler(std::array<int, 5>{}, std::array<int, 3>{}); текущий Clang 7.0 ToT дает следующее, сообщая вам, что размер массива который недействителен:
error: no matching function for call to 'PickyArrayHandler' PickyArrayHandler(std::array<int, 5>{}, std::array<int, 3>{}); ^~~~~~~~~~~~~~~~~ note: candidate template ignored: requirement 'IsValidArrayXSize<5UL>{}' was not satisfied [with SX = 5, SY = 3] void PickyArrayHandler(std::array<int, SX> const& x, std::array<int, SY> const& y) { ^
если вы удалите "класс" из перечисления, вам не понадобится 'GetSizeValue' и приведение