【问题标题】:Compile-time error for non-instantiated template members instead of link-time error非实例化模板成员的编译时错误而不是链接时错误
【发布时间】:2025-12-23 07:40:16
【问题描述】:

我有模板类ItemContainer,它实际上是整个系列容器的外观,具有不同的功能,如排序、索引、分组等。

使用 pimpl idiom 和显式实例化将实现细节隐藏在 cpp. 文件中。模板仅使用众所周知的有限实现类集来实例化,这些实现类定义了容器的实际行为。

主模板实现了所有容器支持的常用功能 - 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

【问题讨论】:

  • 您要解决的问题是标准 C++ 将算法与数据分离的方式无法解决的问题?问题似乎是一个接口太大的类。
  • @MarkB 问题在于接口有大约 10 个通用和专用功能,不值得分成 5 个具有 1-2 个功能的接口。我不想增加这样的实体。

标签: c++ templates visual-c++ c++03


【解决方案1】:

如果在编译时就知道某个函数将不会被实现,那么该函数不应该首先被声明。否则,这是一个编程错误。

话虽如此,您必须避免声明这样的函数,或者声明它以使声明只有在实现时才有效。这可以通过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 */

};

【讨论】:

  • 啊哈,SFINAE。忘了它。不是很优雅,但它有效。谢谢:-)
  • 看起来这是最好的匹配。如果今天没有人提出更好的建议,我会接受。谢谢!
【解决方案2】:

考虑这个例子:

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;

【讨论】:

  • 嗯,这正是我想要避免的——很多定义了 1-2 个函数的类。
  • 查看我答案的最后 3 行。我只使用了一个 ItemContainer - 其余的只是帮助模板来使其工作。您必须定义的函数数量不会改变。
  • 我理解这一点,也考虑过这种变体。例如。 boost::multi_index 使用类似的方法。但希望存在更简单的方法。
  • 谢谢,我有强烈的感觉,这种方法甚至可以减少你的代码行数。我相信(不确定)你可以在 header 中定义模板类函数。
  • 我想将所有函数保留在.cpp 中,并使标题尽可能小而简单,隐藏所有实现细节。它将被包含在许多文件和项目中,我不高兴在修复标题中的 1 个 LOC 后重建数以万计的 LOC...
【解决方案3】:

尽管建议使用 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。

对于带有 C++11 的 GCC,static_assert 的使用需要一些技巧来启用惰性模板函数实例化 (compiled sample):

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!
}

【讨论】:

  • 此解决方案绝不比 SFINAE 解决方案更好(或更差)。两者都获得相同的目标:编译时错误。 static_assert 产生更简洁的编译器错误,但 SFINAE 解决方案提供了更易读的代码(泛型类 ItemContainer 的定义没有暗示 Sort() 通常不会编译)。
  • @Walter 不,它呈现,请参阅标题的结尾:模板 void CSortedOrderContainer::Sort();我不需要为单个成员专业化指定整个模板。
  • SFINAE 结构对于不了解它们的人来说可能看起来很麻烦,但enable_if 是不言自明的。另一方面,您的解决方案不包含(在类定义中)成员 Sort() 不会为类模板的某些模板参数编译的提示。因此,对于大多数人类读者来说,它就不太清楚了。
  • 顺便说一句,BOOST_STATIC_ASSERT 真的很糟糕,因为它 not 会生成一个很好甚至有用的错误消息。如果语言提供了static_assert,为什么还要使用这么糟糕的宏?
  • @Walter 我知道并使用 SFINAE,但它对我来说仍然看起来很丑陋且难以阅读。在生产代码中,我们对每个函数声明都有一个详细的 Doxygen cmets,所以会清楚地解释。使用BOOST_STATIC_ASSERT 是因为它适用于还没有static_assert 的VS2008/C++03。错误消息仅在静态断言之前被注释替换。我们可以用 C++03 做到最好:-(
【解决方案4】:

这个呢?

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 文件中编写实现。

【讨论】:

  • 看起来你有点过于复杂了。对于不同的项目类型,我不需要不同的功能。实际上我从来没有将项目类型作为容器模板参数:-)
  • 确实,但是您希望限制类的数量 :)(所以我分解了一些代码。)ABC 应替换为 SequencedImpl、@ 987654327@SortedImpl(但我没有详细说明哪个类应该实现什么,)因此对于不同的项目类型没有不同的功能。如果类型 T 在 Seq 内,vec_enabler 的答案为 true,否则为 false,用于 SFINAE。 index_cap: 两个访问者的签名是一样的,所以我使用了相同的模板。
  • 如果ImplAB,最后sort_cap&lt;Impl, vec_enabler&lt;Impl, boost::mpl::vector&lt;A, B&gt; &gt; &gt; 添加排序功能(index_cap 相同。)