У меня есть шаблон класса, предназначенный для использования с плавающей запятой.
В классе есть метод, который должен позволять при необходимости расширять входные данные (например, float
-> double
), но не следует допускать сужение (т. е. потерю точности).
Как мне обеспечить соблюдение этого правила?
Следующий пример — лучшее, что мне удалось сделать (используя if constexpr
, то есть хотя бы C++17), но мне кажется, что использование как нереализованной специализации, так и static_cast
, вероятно, не самое чистое и ясное решение.
На самом деле он выполняет описанную работу, при условии, что используются только float
и double
(в отличие от более экзотических типов с плавающей запятой), что для меня не проблема:
#include <vector>
#include <type_traits>
template <typename Real>
class Foo
{
public:
template <typename Real2>
void DoSomething( const std::vector<Real2>& v );
};
// Enter here only when input is of same type as template
template <typename Real>
template <typename Real2>
void Foo<Real>::DoSomething( const std::vector<Real2>& v)
{
if constexpr ( !std::is_same<Real, Real2>::value )
static_assert(false);
// ... Do actual work here ...
}
// Allow widening of inputs
template <>
template <>
void Foo<double>::DoSomething( const std::vector<float>& v)
{
std::vector<double> v2;
v2.reserve( v.size() );
for ( const auto& val : v )
v2.push_back( (double)val );
DoSomething(v2);
}
// Deny narrowing of inputs
template <>
template <>
void Foo<float>::DoSomething( const std::vector<double>& v);
int main()
{
std::vector<float> v1;
std::vector<double> v2;
// ... populate both v1 & v2 ...
Foo<float> b1;
b1.DoSomething( v1 );
//b1.DoSomething( v2 ); // <--- This line should not be allowed to compile
Foo<double> b2;
b2.DoSomething( v1 );
b2.DoSomething( v2 );
}
Как я могу упростить реализацию, чтобы достичь поставленной цели более чистым способом?
Обновлено: изменены неправильные термины «Повышающее приведение» и «Понижающее приведение» на «Расширение» и «Сужение».
Edit2: каким-то образом я отметил неправильную строку, которая, как ожидается, не скомпилируется.
«Вверх» и «вниз» обычно относятся к иерархии наследования. Когда дело доходит до числовых типов, обычными терминами являются «расширение» и «сужение».
@molbdnilo: принял к сведению, отредактировал и исправил терминологию
Вы можете сделать это разными способами, например, с помощью концепций:
#include <vector>
template <typename Real>
class Foo
{
public:
template <typename Real2>
requires requires (Real r) {Real2{r};} // <-- here
void DoSomething( const std::vector<Real2>& v );
};
Или, если именованная концепция выдает более четкое сообщение об ошибке:
template <typename A, typename B>
concept is_not_narrower = requires(B b) {A{b};};
template <typename Real>
class Foo
{
public:
template <typename Real2>
requires is_not_narrower<Real2, Real>
void DoSomething( const std::vector<Real2>& v );
};
Предполагается ли это, что предупреждение о сужении рассматривается как ошибка?
Чтобы добиться полного поведения, следует ли использовать эту концепцию для специализации, которая выполняет сужение, а для реальной работы использовать концепцию «is_same»? Или, скорее, это было бы двусмысленно...
Это не предупреждение. Сужение с помощью инициализации скобок является ошибкой.
Если вам нужно, чтобы поведение/реализация того же типа отличались от поведения при расширении, тогда да, вам нужно специализироваться.
Как бы то ни было, в некоторых крайних случаях для A{b} это выражение правильно сформировано, но B не конвертируется в A. Поэтому стандарт использует A[] a = {b}. eel.is/c++draft/variant.ctor#14 См. также p0870
Термины «восходящее» и «понижающее» имеют в C++ весьма специфическое значение, а преобразование между числовыми типами не является таковым.