Как проверить, существует ли специализация шаблонной функции

Я управляю преобразованием единиц измерения. Скажите нам, что я достиг состояния, когда я этого достиг.

Сердцем моего преобразования между различными единицами измерения является следующая общая функция шаблона:

template <class SrcUnit, class TgtUnit> extern
double convert(double val);

Цель этой функции — преобразовать физическую величину, выраженную в единицах типа SrcUnit, в другую, выраженную в единицах типа TgtUnit.

У меня есть класс Quantity<Unit>, который управляет значениями и их единством, и этот класс пытается обеспечить безопасность типов и автоматическое преобразование. Например, я экспортирую следующий конструктор:

  template <class SrcUnit>
  Quantity(const Quantity<SrcUnit> & q)
    : unit(UnitName::get_instance())
  {
    check_physical_units(q); // verify that the physical magnitudes are the same
    value = convert<SrcUnit, UnitName>(q.value); // <<- here the conversion is done
    check_value(); // check if the value is between the allowed interval
  }

Я экспортирую другие вещи, где выполняется преобразование.

Итак, когда кто-то хочет управлять новым юнитом, он указывает новый производный класс Unit. Я добиваюсь этого, помещая в макрос всю необходимую спецификацию нового класса. Теперь ответственность за написание функций преобразования лежит на этом пользователе. То есть написать две специализации шаблона convert(). Например, предположим, что у вас есть единица измерения под названием «Kilometerand you wish to specify a new unit calledMile». В этом случае вы делаете следующее:

Declare_Unit(Mile, "mi", "English unit of length", Distance,
         0, numeric_limits<double>::max()); // macro instantiating a new Unit class
template <> double convert<Kilometer, Mile>(double val) { return val/1609.344; }
template <> double convert<Mile, Kilometer>(double val) { return 1609.344*val; }

Что произойдет, если пользователь забудет написать функцию преобразования? Что ж, в этом случае компоновщик потерпит неудачу, потому что не сможет найти специализацию convert().

Теперь мой вопрос.

Хотя я думаю, что ошибка компоновщика допустима в качестве поведения для сообщения пользователю об отсутствующем convert(), я хотел бы проверить время компиляции на наличие специализации convert(). Итак, мой вопрос: как я могу этого добиться? Я предполагаю, что через static_assert помещается непосредственно перед каждым вызовом convert(), который проверяет, известна ли уже специализация. Но как это сделать?

PS: Кроме того, мне было бы очень полезно, если бы кто-нибудь порекомендовал мне хороший текст о метапрограммировании C++.


person lrleon    schedule 28.07.2016    source источник
comment
Проверьте ссылку. Там авторы разрабатывают систему преобразования единиц измерения с использованием механизма метапрограммирования. В целом, лучшее введение в TMP, имхо.   -  person p0pa    schedule 28.07.2016
comment
Это похоже на принципиально плохой дизайн. Итак, у вас есть N^2 функции преобразования? Вам нужно реализовать оба от Kilometer до Mile и от Mile до Kilometer по отдельности?   -  person Barry    schedule 28.07.2016
comment
Если физика допускает преобразование N^2, тогда да, @Barry. И по второму вопросу тоже да. Обратите внимание, что бывают случаи, когда преобразование не является строго линейным; хотя это может показаться странным, это относится к некоторым геологическим корреляциям, когда преобразование может быть действительным только внутри эмпирически достоверного интервала. Включительно, есть ситуации, в которых преобразование разрешено только в одном смысле. В любом случае, любые предложения по дизайну очень приветствуются   -  person lrleon    schedule 28.07.2016
comment
Что я имею в виду под N^2, так это то, что если у вас есть N единиц, то каждая единица нуждается в своей собственной функции преобразования в каждую другую единицу... так что у вас есть N^2 различных функций преобразования. Это... не очень хорошо масштабируется.   -  person Barry    schedule 28.07.2016
comment
хорошо @Барри, я понимаю. И да, это не очень хорошо масштабируется, но я считаю, что выхода нет. Некоторые другие системы, которые я исследовал, используют матричное преобразование, которое, хотя и может быть более кратким, все же является квадратичным. В области, в которой я сейчас работаю, впечатляет обилие эмпирических формул и использование различных единиц измерения. С Уважением   -  person lrleon    schedule 28.07.2016


Ответы (4)


Вы можете сделать это, поместив static_assert в основной шаблон функции с небольшим трюком, который гарантирует, что ваша программа не будет плохо сформирована, бессовестно украдена из здесь :

template <typename...> struct always_false : std::false_type {};

template<typename T, typename U> double convert(double) {
  static_assert(always_false<T, U>::value, "No specialization exists!");
}

template <> double convert<Kilometer, Mile>(double val) { return val/1609.344; }
template <> double convert<Mile, Kilometer>(double val) { return 1609.344*val; }

Текущая демонстрация

person 101010    schedule 28.07.2016
comment
static_assert(false,...); неправильно сформирован. - person TartanLlama; 28.07.2016
comment
Ваш начальный foo() имеет неправильный формат, так как static_assert() не зависит от T. - person Barry; 28.07.2016
comment
@TartanLlama это все еще плохо сформировано? - person 101010; 28.07.2016
comment
@ Барри, это все еще плохо сформировано? - person 101010; 28.07.2016
comment
@ 101010 Это нормально :) - person TartanLlama; 28.07.2016
comment
Я только что закончил свою маленькую библиотеку. Я воспользовался вашим подходом. Спасибо вам и всем, кто принимал участие - person lrleon; 30.07.2016

Вы не должны обещать компилятору, что для каждой возможной комбинации SrcUnit/TgtUnit существует преобразование:

template <class SrcUnit, class TgtUnit>
double convert(double val);

Вместо этого вы должны сделать иначе, удалите универсальный преобразователь:

template <class SrcUnit, class TgtUnit>
double convert(double val) = delete;

И для каждого преобразования, которое вы предоставляете, вы должны объявить это в своем заголовочном файле/файлах:

template <>
double convert<Mille,Km>(double val);
template <>
double convert<Milliseconds,Hours>(double val);

Таким образом, компилятору будет известно, что предусмотрено, а что нет.


Дополнительное примечание: ваше исходное решение также может быть подвержено риску нарушения правила одного определения. Компоновщик не смог обнаружить, если вы по ошибке предоставили две разные реализации одного преобразования.

person PiotrNycz    schedule 28.07.2016

Я бы подошел к этому с тегами.

template<class T>struct tag_t{constexpr tag_t(){}; using type=T;};
template<class T>constexpr tag_t<T> tag{};

это позволяет вам передавать типы как параметры функции.

Теперь вместо использования специализации и тому подобного мы просто вызываем функцию.

Мы будем использовать имя получше, чем convert, например unit_convert.

namespace my_ns {
  Declare_Unit(Mile, "mi", "English unit of length", Distance,
     0, numeric_limits<double>::max()); // macro instantiating a new Unit 
  inline double unit_convert( tag_t<Meter>, tag_t<Mile>, double val ) {
    return val/1609.344
  }
  inline double unit_convert( tag_t<Mile>, tag_t<Meter>, double val ) {
    return val*1609.344
  }
}

Затем мы пишем трейт, который говорит, существует ли unit_convert( tag<A>, tag<B>, double ):

namespace details {
  template<template<class...>class Z, class=void, class...Ts>
  struct can_apply:std::false_type{};
  template<template<class...>class Z,  class...Ts>
  struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:std::true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z,void,Ts...>;

template<class...Args>
using convert_unit_result = decltype( convert_unit( std::declval<Args>()... ) );

template<class SourceUnit, class DestUnit>
using can_convert_units = can_apply< convert_unit_result, tag_t<SourceUnit>, tag_t<DestUnit>, double >;

и теперь can_convert_units<Mile,Meter> — это std::true_type, а can_conver_units<Chicken, Egg> — это std::false_type (при условии, что курица и яйцо — это типы, которые не являются конвертируемыми единицами, естественно).

В качестве бонуса этот метод позволяет вам размещать ваши единицы измерения в отдельных пространствах имен, а также определять их функции преобразования. Если и Mile, и Meter попытаются определить преобразование между собой, вы получите ошибки при попытке преобразования (неоднозначно). Функция преобразования может находиться в любом пространстве имен, но не в обоих.

person Yakk - Adam Nevraumont    schedule 28.07.2016

Может быть, это может помочь.

template<typename Src, typename Tgt, typename Exists = void>
double convert (double val) {
    static_assert(not std::is_void<Exists>::value, " err msg");
}

template <> double convert<Kilometer, Mile>(double val) { return val/1609.344; }
person Kabik    schedule 28.07.2016