C++: устранение неоднозначности частичной специализации вручную (с помощью SFINAE)

Я реализую универсальный класс, который должен вести себя по-разному для разных наборов типов (не только для разных дискретных типов). Цель состоит в том, чтобы сериализовать объекты разных типов, чтобы отправлять их по пользовательскому протоколу (но это скорее образовательная задача, чем что-то прагматическое; я студент, который интересуется распределенными вычислениями). Например, мне нужно по-разному отправлять числа с плавающей запятой и целые числа. Я также хочу иметь обработчик по умолчанию для других типов POD. Но мне нужно переопределить поведение для некоторых типов моих POD...

Я обнаружил, что метод SFINAE очень полезен и реализует общие классы для целочисленных типов и типов с плавающей запятой, для моих пользовательских типов с использованием принципа SFINAE и частичной специализации (см. код ниже). Но когда я попытался реализовать обработчик других типов POD, надеясь, что другие обработчики будут перекрывать более общий обработчик POD-типов, я столкнулся с проблемой неоднозначности. На самом деле нет никакого пересечения возможных специализаций моего класса GenericObject для типов POD и его подмножеств - целочисленных типов и типов с плавающей запятой.

Я пытался реализовать ручное упорядочивание специализаций, много читал о частичном упорядочивании, о том, что одна специализация предпочтительнее другой, если она более специализированная. Но мне не удалось решить проблему. У меня нет идей, как устранить неоднозначность моих частичных специализаций вручную.

Решение с исключением набора типов с плавающей запятой и целочисленных типов моего обработчика POD-типов для меня неприемлемо, т.к. таким образом получается лишняя зависимость между обработчиками. Я надеюсь, что есть правильный способ решить мою проблему. Например, при старте программы все статические ресурсы инициализируются с несколькими приоритетами. В GCC я могу управлять последовательностью такой инициализации с помощью конструктора атрибутов: __ attribute__((constructor(101))) или аналогичный атрибут init_priority. Я был бы рад, если бы я мог переупорядочить частичную специализацию шаблона таким образом.

Не могли бы вы предложить мне что-нибудь?

Вот мой код:

#include <type_traits>
#include <iostream>
#include <cxxabi.h>

// General form
template <typename T, typename Enable0 = void>
struct GenericObject {
    char * description() {
        return (char *)"Undefined";
    }
};

// Specialization for integral types
template <typename T>
struct GenericObject<T, typename std::enable_if<std::is_integral<T>::value>::type> {
    char * description() {
        return (char *)"Integral";
    }
};

// Specialization for real types
template <typename T>
struct GenericObject<T, typename std::enable_if<std::is_floating_point<T>::value>::type> {
    char * description() {
        return (char *)"Real";
    }
};

// Specialization for other POD types. It MUST be less specialized than specializations for real and integral types, because in other way there will be an ambiguity, because every integral type is also a POD.
/*

    HERE IS MY PROBLEM

*/
template <typename T>
struct GenericObject<T, typename std::enable_if<std::is_pod<T>::value>::type> {
    char * description() {
        return (char *)"POD";
    }
};

// Declaration of types
struct IAmDefined {};
struct IAmUndefinedPOD {};
struct IAmUndefinedComplexClass : virtual IAmUndefinedPOD {};

// Specialization for IAmDefined class and also the most specialized template specialization.
template <>
struct GenericObject<IAmDefined> {
    char * description() {
        return (char *)"Defined";
    }
};

// Produces nice output
std::string demangle(const char *raw) {
    int status;

    char *demangled = abi::__cxa_demangle(raw, 0, 0, &status);
    std::string result(demangled);
    free(demangled);

    return result;
}

template <typename T>
void testObject() {
    GenericObject<T> object;
    std::cout << demangle(typeid(T).name()) << ": " << object.description() << std::endl;
}

int main() {    
    testObject<int>(); // Integral
    testObject<long>(); // Integral
    testObject<float>(); // Real
    testObject<double>(); // Real
    testObject<void>(); // POD
    testObject<IAmDefined>(); // Defined
    testObject<IAmUndefinedPOD>(); // POD
    testObject<IAmUndefinedComplexClass>(); // Undefined
}

Вот ошибки времени компиляции:

g++ --std=c++11 main.cc -o specialization-of-sets
main.cc: In instantiation of 'void testObject() [with T = int]':
main.cc:85:21:   required from here
main.cc:68:22: error: ambiguous class template instantiation for 'struct GenericObject<int, void>'
main.cc:15:8: error: candidates are: struct GenericObject<T, typename std::enable_if<std::is_integral<_Tp>::value>::type>
main.cc:36:8: error:                 struct GenericObject<T, typename std::enable_if<std::is_pod<_Tp>::value>::type>
main.cc:68:22: error: 'GenericObject<int, void> object' has incomplete type
main.cc: In instantiation of 'void testObject() [with T = long int]':
main.cc:86:22:   required from here
main.cc:68:22: error: ambiguous class template instantiation for 'struct GenericObject<long int, void>'
main.cc:15:8: error: candidates are: struct GenericObject<T, typename std::enable_if<std::is_integral<_Tp>::value>::type>
main.cc:36:8: error:                 struct GenericObject<T, typename std::enable_if<std::is_pod<_Tp>::value>::type>
main.cc:68:22: error: 'GenericObject<long int, void> object' has incomplete type
main.cc: In instantiation of 'void testObject() [with T = float]':
main.cc:87:23:   required from here
main.cc:68:22: error: ambiguous class template instantiation for 'struct GenericObject<float, void>'
main.cc:23:8: error: candidates are: struct GenericObject<T, typename std::enable_if<std::is_floating_point<_Tp>::value>::type>
main.cc:36:8: error:                 struct GenericObject<T, typename std::enable_if<std::is_pod<_Tp>::value>::type>
main.cc:68:22: error: 'GenericObject<float, void> object' has incomplete type
main.cc: In instantiation of 'void testObject() [with T = double]':
main.cc:88:24:   required from here
main.cc:68:22: error: ambiguous class template instantiation for 'struct GenericObject<double, void>'
main.cc:23:8: error: candidates are: struct GenericObject<T, typename std::enable_if<std::is_floating_point<_Tp>::value>::type>
main.cc:36:8: error:                 struct GenericObject<T, typename std::enable_if<std::is_pod<_Tp>::value>::type>
main.cc:68:22: error: 'GenericObject<double, void> object' has incomplete type

Я использую GCC 4.8:

версия gcc 4.8.0 20120314 (экспериментальная) [версия магистрали 185382] (Ubuntu/Linaro 20120314-0ubuntu2)

Заранее спасибо.


person cppist    schedule 27.10.2012    source источник
comment
Почему вы приводите строковые литералы к char*?!   -  person Xeo    schedule 27.10.2012
comment
Вы можете добавить !is_scalar или что-то в этом роде.   -  person Kerrek SB    schedule 27.10.2012
comment
Почему вы используете строковые литералы. Это только для примера, я никогда не использую такое преобразование в своих проектах. Без этого преобразования GCC выдает предупреждения со стандартными опциями. Таким образом, это преобразование кажется самым простым способом избежать предупреждений в примерах кода.   -  person cppist    schedule 27.10.2012
comment
Я каждый раз терпел неудачу. Использование const char * вместо char * решило проблему с предупреждениями...   -  person cppist    schedule 27.10.2012


Ответы (2)


Я лично решил бы это с помощью перегрузки, а не частичной специализации шаблона класса. Таким образом, вы можете легко ввести тай-брейк:

template<class T> struct type{};

namespace detail{
template<class T> using Invoke = typename T::type;
template<class C, class T = void> using EnableIf = Invoke<std::enable_if<C::value, T>>;
template<class T> using EType = type<EnableIf<T>>;

// we need two tie-breakers here, one for general vs specialized case
// and one for specialized vs more specialized case
template<class T>
char const* describe(EType<std::is_integral<T>>, int, int){ return "Integral"; }
template<class T>
char const* describe(EType<std::is_floating_point<T>>, int, int){ return "Real"; }
template<class T>
char const* describe(EType<std::is_pod<T>>, int, long){ return "POD"; }
template<class T>
char const* describe(type<void>, long, long){ return "Undefined"; }
} // detail::

template<class T>
char const* describe(type<T>){
  // literal '0' is 'int', as such 'int' overloads are preferred
  return detail::describe<T>(type<void>(), 0, 0);
}

// ...

// simple overload for specialized types
char const* describe(type<IAmDefined>){
  return "Defined";
}

Живой пример. Обратите внимание, что void относится к категории "Undefined", это не POD.

person Xeo    schedule 27.10.2012
comment
Да, я ошибся с пустотой. Большое спасибо. Я понял, как это работает, на gcc 4.8 работает отлично. - person cppist; 27.10.2012

Я всегда вручную разрешал неоднозначности с помощью std::enable_if, но этот подход может стать шаблонным, когда у вас есть много альтернатив:

#include <iostream>
#include <type_traits>

using namespace std;

template<typename T>
const char* describe(T,
                     typename enable_if<is_integral<T>::value>::type* = 0)
{
    return "integral";
}

template<typename T>
const char* describe(T,
                     typename std::enable_if<is_floating_point<T>::value>::type* = 0)
{
    return "floating_point";
}

template<typename T>
const char* describe(T,
                     typename std::enable_if<!std::is_integral<T>::value && !std::is_floating_point<T>::value && std::is_pod<T>::value>::type* = 0)
{
    return "pod";
}

http://ideone.com/jb70Cd

person DikobrAz    schedule 04.11.2014