如何确定模板参数是否为默认值

How can I determine if a template parameter is defaulted or not?

本文关键字:是否 默认值 参数 何确定      更新时间:2023-10-16

我有这个简化的类(省略了许多细节):

template<class T, size_t nChunkSize = 1000>
class Holder
{
    size_t m_nSize = 0;
    size_t m_nChunkSize = nChunkSize;
public:
    Holder(size_t nSize)
        : m_nSize(nSize)
    {
    }
    size_t GetChunkSize()
    {
        return m_nChunkSize;
    }
    T* GetChunk(size_t nChunkIndex)
    {
        // returns the address of the chunk nChunkIndex
        return ###;
    }
    T& operator[](size_t nIndex)
    {
        // returns the element with index nIndex
        return T();
    }
};

这个想法是有一个简单的内存管理器来分配大量对象,但如果没有足够的内存将所有对象保存在一个地方,它会将它们分成块并封装所有内容。我知道我应该使用STL,但我有具体的理由这样做。

我希望

为用户提供指定块大小并能够获取指向特定块的指针的能力,但前提是他们指定了模板参数,否则我希望在编译时禁用该功能。

我知道编译器应该知道 nChunkSize 是默认的还是用户指定的,但是有没有办法获取该信息并使用它来删除 GetChunk 函数或使其用法不可编译。

例如:

Holder<int, 200> intHolder(5000); // allocates 5000 integeres each chunk holds 200 of them
intHolder[312] = 2;
int* pChunk = intHolder.GetChunk(3); // OK, Compiles
Holder<int> intAnotherHolder(500); // allocates 500 but chunk size is hidden/implementation defined
pChunk = intAnotherHolder.GetChunk(20); // COMPILE ERROR

可以使用具有两个派生类的公共基类:一个专门用于提供size_t的方案,另一个专门用于未提供派生类的方案:

基础(基本上是你当前的类):

template<typename T, size_t nChunkSize=1000>
class Base
{
    size_t m_nSize = 0;
    size_t m_nChunkSize = nChunkSize;
public:
    Base(size_t nSize)
        : m_nSize(nSize)
    {
    }
    size_t GetChunkSize()
    {
        return m_nChunkSize;
    }

    T& operator[](size_t nIndex)
    {
        // returns the element with index nIndex
        return T();
    }
};

默认(无法调用GetChunk):

// empty argument list
template<typename T, size_t... ARGS>
class Holder : public Base<T>
{
   static_assert(sizeof...(ARGS) == 0, "Cannot instantiate a Holder type with more than one size_t");
    using Base<T>::Base;
};

非默认值(具有GetChunk方法):

template<typename T, size_t nChunkSize>
class Holder<T, nChunkSize> : public Base<T, nChunkSize>
{
    using Base<T>::Base;
    public:
    T* GetChunk(size_t nChunkIndex)
    {
        // returns the address of the chunk nChunkIndex
        return nullptr;
    }
};

演示

如果nChunkSize是类型模板参数,则可以使用默认标记并基于该标记工作。由于它是一个非类型参数,因此可以使用标志值作为默认值,然后在类定义中更正它:

template<class T, size_t nChunkSize = std::numeric_limits<size_t>::max()>
//                         flag value ^--------------------------------^
class Holder
{
    size_t m_nSize = 0;
    size_t m_nChunkSize = 
        nChunkSize == std::numeric_limits<size_t>::max() ? 1000 : nChunkSize;
        //^If the flag value was used, correct it
    T* GetChunk(size_t nChunkIndex)
    {
        //Check if the flag value was used
        static_assert(nChunkSize != std::numeric_limits<size_t>::max(), 
                      "Can't call GetChunk without providing a chunk size");
        // return the address of the chunk nChunkIndex
    }

如果没有传递默认参数,这将使GetChunk编译失败。当然,如果您将最大size_t传递给Holder,那么它将静默地固定到 1000 ,但大概您不打算传递那么高的值。

现场演示

我建议使用两个不同的类:如果它们应该有不同的实现,为什么要坚持一个单一的定义?

template<class T, size_t nChunkSize>
class ChunkHolder
{
    size_t m_nSize = 0;
    size_t m_nChunkSize = nChunkSize;
public:
    ChunkHolder(size_t nSize) : m_nSize(nSize) {}
    size_t GetChunkSize() { return m_nChunkSize; }
    // returns the address of the chunk nChunkIndex
    T* GetChunk(size_t nChunkIndex) { return nullptr; }
    // returns the element with index nIndex
    T& operator[](size_t nIndex)    { return T(); }
};
template<class T>
class UnchunkHolder
{
    size_t m_nSize = 0;
public:
    UnchunkHolder(size_t nSize) : m_nSize(nSize) {}
    // returns the address of the chunk nChunkIndex
    T& operator[](size_t nIndex)    { return T(); }
};

然后,我们定义帮助程序函数来创建一个或多个类:

template <typename T, size_t SIZE> ChunkHolder<T, SIZE>
Holder(size_t nSize) { return {nSize}; }
template <typename T>              UnchunkHolder<T>
Holder(size_t nSize) { return {nSize}; }

最后,我们可以这样使用它:

auto x = Holder<int, 200u>(5000u);
auto y = Holder<int>(500u);

x是块功能的Holder 1y缺少该功能,并且无法编译GetChunk调用,只是因为底层类型缺少该函数。

在此处查看现场演示。


  1. 好吧,它不是,是一个ChunkHolder,你可以创建一个具有通用实现(operator[]...)的基类或使用不同的类;这取决于你的实现需求。

基本上没有标准方法来知道编译器是否添加了默认值,或者用户是否实际键入了默认值。当你的代码可以开始区分时,价值已经存在。

特定的编译器可以提供这样的钩子(例如__is_defaulted(nChunkSize)),仔细检查编译器文档可能会有所帮助,但常见的编译器似乎不提供这样的功能。


我不确定用例的确切性质;但"通常"的选择是使用部分模板专业化来区分实现,而不是真正关心nChunkSize的价值来自哪里而是价值是什么

#include <iostream>
using namespace std;
template <typename T, size_t nChunkSize = 1000>
struct A {
    A() { cout << "A()" << endl; }
};
template <typename T>
struct A<T, 1000> {
    A() { cout << "A() special" << endl; }
};
int main() {
    A<int, 100> a1; // prints A()
    A<int> a2; // prints A() special
    return 0;
}

演示示例。可以根据需要将更多常见详细信息移动到特征类或基类。


但是,以上并不能完全实现您想要的。使您更接近目标帖子的替代方法包括使用可用于区分用户提供的值或使用默认值的"特殊"值。理想情况下,该值是用户不太可能使用它; 在这种情况下,我想到了0。它仍然不能保证用户会使用0,但在"合理"的客户端代码中不太可能有0块大小。

template<class T, size_t nChunkSize = 0>
class Holder
{
    size_t m_nSize = 0;
    size_t m_nChunkSize = nChunkSize == 0 ? 1000 : nChunkSize;
    // ...

然后可以使用static_assert根据nChunkSize的值允许是否编译GetChunk - 这是有效的,因为nChunkSize在编译时是已知的。

T* GetChunk(size_t nChunkIndex)
{
    static_assert(nChunkSize != 0, "Method call invalid without client chunk size");
    // ...

缺点是GetChunk在开发过程中仍然"可见",但如果调用它,编译将失败。


你可以得到的最接近的已经提到过,但在这里重复进行比较;是将类的实现推迟到某个BaseHolder,然后将其与部分模板专用化相结合,以确定客户端代码是否使用了块大小(nChunkSize的值)或不。

template<typename T, size_t nChunkSize /*=1000*/>
// default not provided here, it is not needed
class BaseHolder
{
    size_t m_nSize = 0;
    size_t m_nChunkSize = nChunkSize;
    // ...
};
template<typename T, size_t... ARGS>
class Holder : public Base<T, 1000>
{
    // "default" value for nChunkSize required
    // When sizeof...(ARGS) = 1, the specialisation is used
    //   when 0, the client has not provided the default
    //   when 2 or more, it is invalid usage
    static_assert(sizeof...(ARGS) == 0, "Only 1 size allowed in the client code");
    // ...
};
template<typename T, size_t nChunkSize>
class Holder<T, nChunkSize> : public Base<T, nChunkSize>
{
    // non-default chunk size used (could still be 1000)
    // includes the implementation of GetChunk
public:
    T* GetChunk(size_t nChunkIndex)
    {
        // ...
    }
};

这种方法的缺点是可以提供多个size_t参数,这可以在编译时用static_assert控制;代码的文档也应该清楚地说明这一点。