Ошибка времени компиляции для неэкземплярных элементов шаблона вместо ошибки времени компоновки

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

Детали реализации скрыты в файле cpp. с использованием идиомы pimpl и явного создания экземпляров. Шаблон создается только с хорошо известным ограниченным набором классов реализации, которые определяют фактическое поведение контейнера.

Основной шаблон реализует общие функции, поддерживаемые всеми контейнерами — IsEmpty(), GetCount(), Clear() и т. д.

Каждый конкретный контейнер специализируется на некоторых функциях, которые поддерживаются только им, например. Sort() для отсортированного контейнера, operator[Key&] для индексированного по ключу контейнера и т. д.

Причина такого дизайна в том, что класс является заменой нескольким устаревшим велосипедным контейнерам ручной работы, написанным какими-то доисториками в начале 90-х. Идея состоит в том, чтобы заменить старую гниющую реализацию современными контейнерами STL&Boost, максимально сохранив старый интерфейс нетронутым.

Проблема

Такой дизайн приводит к неприятной ситуации, когда пользователь пытается вызвать неподдерживаемую функцию из какой-то специализации. Компилируется нормально, но на этапе линковки выдает ошибку (символ не определен). Не очень удобное поведение.

Пример:

 SortedItemContainer sc;
 sc.IsEmpty(); // OK
 sc.Sort(); // OK

 IndexedItemContainer ic;
 ic.IsEmpty(); // OK
 ic.Sort(); // Compiles OK, but linking fails

Конечно, этого можно было бы полностью избежать, используя наследование вместо специализации, но я не люблю создавать много классов с 1-3 функциями. Хотелось бы сохранить оригинальный дизайн.

Есть ли возможность превратить его в ошибку этапа компиляции вместо первого этапа ссылки? У меня есть ощущение, что статическое утверждение можно как-то использовать.

Целевым компилятором для этого кода является VS2008, поэтому практическое решение должно быть совместимо с C++03 и может использовать специфические функции MS. Но портативные решения C++11 также приветствуются.

Исходный код:

// ItemContainer.h
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

template <class Impl> class ItemContainer
{
public:

   // Common functions supported by all specializations
   void Clear();
   bool IsEmpty() const;
   ...

   // Functions supported by sequenced specializations only
   ItemPtr operator[](size_t i_index) const; 
   ...

   // Functions supported by indexed specializations only
   ItemPtr operator[](const PrimaryKey& i_key) const;
   ...

   // Functions supported by sorted specializations only
   void Sort();
   ...

private:

   boost::scoped_ptr<Impl> m_data; ///< Internal container implementation

}; // class ItemContainer

// Forward declarations for pimpl classes, they are defined in ItemContainer.cpp
struct SequencedImpl;
struct IndexedImpl;
struct SortedImpl;

// Typedefs for specializations that are explicitly instantiated
typedef ItemContainer<SequencedImpl> SequencedItemContainer;
typedef ItemContainer<IndexedImpl> IndexedItemContainer;
typedef ItemContainer<SortedImpl> SortedItemContainer;

// ItemContainer.cpp
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Implementation classes definition, skipped as non-relevant
struct SequencedImpl { ... };
struct IndexedImpl { ... };
struct SortedImpl { ... };

// Explicit instantiation of members of SequencedItemContainer
template  void SequencedItemContainer::Clear(); // Common
template  bool SequencedItemContainer::IsEmpty() const; // Common
template  ItemPtr SequencedItemContainer::operator[](size_t i_index) const; // Specific

// Explicit instantiation of members of IndexedItemContainer
template  void IndexedItemContainer::Clear(); // Common
template  bool IndexedItemContainer::IsEmpty() const; // Common
template  ItemPtr IndexedItemContainer::operator[](const PrimaryKey& i_key) const; // Specific

// Explicit instantiation of members of SortedItemContainer
template  void SortedItemContainer::Clear(); // Common
template  bool SortedItemContainer::IsEmpty() const; // Common
template  void SortedItemContainer::Sort(); // Specific

// Common functions are implemented as main template members
template <class Impl> bool ItemContainer<Impl>::IsEmpty() const
{
   return m_data->empty(); // Just sample
}

// Specialized functions are implemented as specialized members (partial specialization)
template <> void SortedItemContaner::Sort()
{
   std::sort(m_data.begin(), m_data.end(), SortFunctor()); // Just sample
}

...
// etc

person Rost    schedule 27.09.2012    source источник
comment
Какую проблему вы пытаетесь решить, которая не решается стандартным способом C++ отделения алгоритмов от данных? Проблема заключается в том, что класс имеет слишком большой интерфейс.   -  person Mark B    schedule 28.09.2012
comment
@MarkB Проблема заключается в интерфейсе с примерно 10 общими и специализированными функциями, который не стоит разбивать на 5 интерфейсов с 1-2 функциями. Я хотел бы не множить таких сущностей.   -  person Rost    schedule 28.09.2012


Ответы (4)


Если во время компиляции известно, что определенная функция не будет реализована, то эта функция не должна была быть объявлена ​​в первую очередь. В противном случае это ошибка программирования.

Сказав это, вы должны избегать объявления такой функции или объявлять ее так, чтобы объявление работало только в том случае, если оно будет реализовано. Это может быть достигнуто либо с помощью static_assert, либо с помощью SFINAE. Например

template<class Container>   // you need one instantination per container supported
struct container_traits
{
   static const bool has_sort;  // define appropriately in instantinations
   /* etc */
};

template<class container>
class ContainerWrapper {

  unique_ptr<container> _m_container;

  template<bool sorting> typename std::enable_if< sorting>::type
  _m_sort()
  {
    _m_container->sort();
  }

  template<bool sorting> typename std::enable_if<!sorting>::type
  _m_sort()
  {
    static_assert(0,"sort not supported");
  }

public

  void sort()
  {
    _m_sort<container_traits<container>::has_sort>();
  }

  /* etc */

};
person Walter    schedule 27.09.2012
comment
Ага, СФИНАЕ. Забыл об этом. Не очень элегантно, но работает. Спасибо :-) - person Rost; 28.09.2012
comment
Похоже, это лучший матч. Я соглашусь, если сегодня никто не предложит что-то лучше. Спасибо! - person Rost; 28.09.2012

Рассмотрим этот пример:

class A {
public:
  void foo() {}
  void bar();
};

Только на этапе связывания может быть обнаружена ошибка, что A::bar() не определено и это не имеет отношения к шаблонам.

Вы должны определить отдельные интерфейсы для разных контейнеров и использовать их для своих реализаций. Только одна из возможностей ниже:

template <class Impl> 
class ItemContainerImpl
{
public:
   ItemContainerImpl();
protected:
   boost::scoped_ptr<Impl> m_data; ///< Internal container implementation
};

// No operations
template <class Impl>
class Empty : protected virtual ItemContainerImpl<Impl> {};

template <class Impl, template <class> class Access, template <class> class Extra = Empty> 
class ItemContainer : public Extra<Impl>, public Access<Impl>
{
public:

   // Common functions supported by all specializations
   void Clear();
   bool IsEmpty() const;
   ...
};

template <class Impl>
class SequencedSpecialization : protected virtual ItemContainerImpl<Impl> {
public:
   // Functions supported by sequenced specializations only
   ItemPtr operator[](size_t i_index) const; 
   ...
};


template <class Impl>
class IndexedSpecialization : protected virtual ItemContainerImpl<Impl> {
public:
   // Functions supported by indexed specializations only
   ItemPtr operator[](const PrimaryKey& i_key) const;
   ...
};

template <class Impl>
class Sorted : protected virtual ItemContainerImpl<Impl> {
public:
   // Functions supported by sorted specializations only
   void Sort();
   ...
};

// Typedefs for specializations that are explicitly instantiated
typedef ItemContainer<SequencedImpl, SequencedSpecialization> SequencedItemContainer;
typedef ItemContainer<IndexedImpl, IndexedSpecialization> IndexedItemContainer;
typedef ItemContainer<SortedImpl, IndexedSpecialization, Sorted> SortedItemContainer;
person PiotrNycz    schedule 27.09.2012
comment
Ну, это именно то, чего я пытаюсь избежать - множество классов с 1-2 определенными функциями. - person Rost; 28.09.2012
comment
Смотрите последние 3 строки моего ответа. Я использую только один ItemContainer, остальные — просто вспомогательные шаблоны, чтобы все работало. Количество функций, которые вы должны определить, не изменится. - person PiotrNycz; 28.09.2012
comment
Я это понимаю и рассматривал и такой вариант. Например. boost::multi_index использует аналогичный подход. Но надеюсь, что существует более простой способ. - person Rost; 28.09.2012
comment
Спасибо, у меня есть сильное ощущение, что этот подход может даже уменьшить количество ваших строк кода. Я считаю (не уверен), что вы можете определить функции классов шаблонов в заголовке. - person PiotrNycz; 28.09.2012
comment
Я хотел бы сохранить все функции в .cpp и сделать заголовок как можно меньше и проще, скрывая все детали реализации. Он будет включен во многие файлы и проекты, и мне не приятно перестраивать миллионы LOC после исправления 1 LOC в заголовке... - person Rost; 28.09.2012

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

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

Что было сделано:

  1. Добавлена ​​фиктивная реализация определенных функций для основного шаблона. Реализации, содержащие только статические утверждения, были помещены в заголовочный файл, но не встроены в определение класса.
  2. Явные экземпляры определенных функций были удалены из файла .cpp.
  3. В заголовочный файл добавлены объявления специализации конкретных функций.

Исходный код:

// ItemContainer.h
//////////////////////////////////////////////////////////////////////////////
template <class Impl> class ItemContainer
{
public:

   // Common functions supported by all specializations
   void Clear();
   bool IsEmpty() const;
   ...

   // Functions supported by sorted specializations only
   void Sort();
   ...

private:

   boost::scoped_ptr<Impl> m_data; ///< Internal container implementation

}; // class ItemContainer

// Dummy implementation of specialized function for main template
template <class Impl> void ItemContainer<Impl>::Sort()
{
   // This function is unsupported in calling specialization
   BOOST_STATIC_ASSERT(false);
}

// Forward declarations for pimpl classes,
// they are defined in ItemContainer.cpp
struct SortedImpl;

// Typedefs for specializations that are explicitly instantiated
typedef ItemContainer<SortedImpl> SortedItemContainer;

// Forward declaration of specialized function member
template<> void CSortedOrderContainer::Sort();

// ItemContainer.cpp
//////////////////////////////////////////////////////////////////////////////

// Implementation classes definition, skipped as non-relevant
struct SortedImpl { ... };

// Explicit instantiation of common members of SortedItemContainer
template  void SortedItemContainer::Clear();
template  bool SortedItemContainer::IsEmpty() const;

// Common functions are implemented as main template members
template <class Impl> bool ItemContainer<Impl>::IsEmpty() const
{
   return m_data->empty(); // Just sample
}

// Specialized functions are implemented as specialized members
// (partial specialization)
template <> void SortedItemContaner::Sort()
{
   std::sort(m_data.begin(), m_data.end(), SortFunctor()); // Just sample
}

...
// etc

Таким образом, это работает по крайней мере для VS2008.

Для использования GCC с C++11 static_assert требуется некоторая хитрость, чтобы включить ленивую установку функции шаблона (скомпилированный пример):

template <class T> struct X
{
    void f();
};

template<class T> void X<T>::f()
{
   // Could not just use static_assert(false) - it will not compile.
   // sizeof(T) == 0 is calculated only on template instantiation and       
   // doesn't produce immediate compilation error
   static_assert(sizeof(T) == 0, "Not implemented");
}

template<> void X<int>::f()
{
  std::cout << "X<int>::f() called" << std::endl;
}

int main()
{
   X<int> a;
   a.f(); // Compiles OK

   X<double> b;
   b.f(); // Compilation error - Not implemented!
}
person Rost    schedule 16.10.2012
comment
Это решение ничем не лучше (или хуже) решения SFINAE. Оба достигают одной и той же цели: ошибка времени компиляции. static_assert выдает более аккуратную ошибку компилятора, но решение SFINAE дает более читаемый код (определение вашего универсального класса ItemContainer не содержит намека на то, что Sort() обычно не компилируется). - person Walter; 17.10.2012
comment
@Walter Нет, это представлено, см. Конец заголовка: template‹› void CSortedOrderContainer::Sort(); Мне не нужно указывать весь шаблон для специализации одного члена. - person Rost; 17.10.2012
comment
Конструкции SFINAE могут показаться громоздкими для тех, кто их не знает, но enable_if говорит сам за себя. Ваше решение, с другой стороны, не содержит подсказки (в определении класса), что член Sort() не будет компилироваться для определенных аргументов шаблона шаблона класса. Таким образом, большинству читателей это гораздо менее понятно. - person Walter; 17.10.2012
comment
Кстати, BOOST_STATIC_ASSERT действительно ужасен, так как он не генерирует красивое или даже полезное сообщение об ошибке. Зачем использовать такой ужасный макрос, если язык предоставляет static_assert? - person Walter; 17.10.2012
comment
@Walter Я знаю и использую SFINAE, но для меня он все еще выглядит уродливым и трудно читаемым. В производственном коде у нас есть подробные комментарии Doxygen для каждого объявления функции, так что это будет понятно объяснено. BOOST_STATIC_ASSERT используется, потому что он предназначен для VS2008/C++03, в котором еще нет static_assert. Сообщение об ошибке просто заменяется комментарием прямо перед статическим утверждением. Лучшее, что мы можем иметь с С++ 03 :-( - person Rost; 17.10.2012

Что насчет этого ?

template <class T, class supported_types> struct vec_enabler : 
  boost::mpl::contains<supported_types, T> {};

// adding Sort interface
template <class T, class enabler, class Enable = void>
struct sort_cap{};

template <class T, class enabler>
struct sort_cap<T, enabler, 
                typename boost::enable_if< typename enabler::type >::type>
{
  void Sort();
};

// adding operator[]
template <class T, class U, class R, class enabler, class Enable = void>
struct index_cap{};

template <class T, class primary_key, class ret, class enabler>
struct index_cap<T, primary_key, ret, enabler, 
                 typename boost::enable_if< typename enabler::type >::type>
{
  ret operator[](primary_key i_index) const;
};


template <class Impl> 
class ItemContainer : 
  public sort_cap<Impl, 
                  vec_enabler<Impl, boost::mpl::vector<A, B> > >, // sort for classes A or B
  public index_cap<Impl, size_t, ItemPtr, 
                   vec_enabler<Impl, boost::mpl::vector<C> > >, // index for class C
  public index_cap<Impl, primaryKey, ItemPtr, 
                   vec_enabler<Impl, boost::mpl::vector<B> > > // index for class B
{
public:
  void Clear();
  bool IsEmpty() const;
}; 

Я считаю, что использование наследования - это самый чистый способ добиться того, что вы хотели бы сделать (то есть «добавления интерфейсов к классу»). Тогда у нас есть следующее:

int main(){
    ItemContainer<A> cA;
    cA.Sort();

    //ItemPtr p = cA[0]; // compile time error

    ItemContainer<C> cC;
    //cC.Sort(); // compile time error
    ItemPtr p = cC[0];
    //ItemPtr pp= cC[primaryKey()]; // compile time error
}

Конечно, вы по-прежнему можете написать реализацию в файлах .cpp.

person Raffi    schedule 02.10.2012
comment
Похоже, вы немного усложнили ситуацию. Мне не нужны разные возможности для разных типов предметов. На самом деле у меня никогда не было типа элемента в качестве параметра шаблона контейнера :-) - person Rost; 02.10.2012
comment
Действительно, но вы хотели, чтобы количество классов было ограничено :) (поэтому я факторизовал некоторый код.) A, B и C следует заменить на SequencedImpl, IndexedImpl SortedImpl (но я не вдавался в подробности, какой класс должен реализовывать что ,) поэтому нет разных возможностей для разных типов предметов. vec_enabler — это то, что отвечает true, если тип T находится внутри Seq, и false в противном случае, и используется для SFINAE. index_cap: подпись одинакова для обоих аксессоров, поэтому я использовал один и тот же шаблон. - person Raffi; 02.10.2012
comment
Наконец, sort_cap<Impl, vec_enabler<Impl, boost::mpl::vector<A, B> > > добавляет возможности сортировки, если Impl равно A или B (то же самое для index_cap). - person Raffi; 02.10.2012