Reputation: 998
I have this simplified class (many details omitted) :
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();
}
};
The idea is to have a simple memory manager that allocates really large number of objects but if there is not enough memory to hold all objects in one place it splits them in chunks and encapsulates everything. I know I should use STL but I have specific reasons to do it this way.
I want to provide the users the ability to specify the chunk size and be able to get a pointer to a specific chunk but only if they have specified the template parameter otherwise I want that functionality to be disabled at compile time.
I know the compiler should know whether nChunkSize is defaulted or user specified but is there a way I can get that information and use it to delete GetChunk function or make it's usage not compilable.
For example:
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
Upvotes: 3
Views: 752
Reputation: 30605
There is essentially no standard way of knowing whether the compiler added the default value or if the user physically typed it in. By the time you code could start to differentiate, the value is already there.
A specific compiler could offer such a hook (e.g. something like __is_defaulted(nChunkSize)
), careful examination of your compiler documentation may help, but the common compilers don't seem to offer such a facility.
I'm not sure the exact nature of the use case; but the "usual" option is to use partial template specialisation to differentiate between the implementations and not to really care where the value came from for nChunkSize
, but rather what the value is.
#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;
}
Demo sample. Further common details can be moved to a traits class or a base class as desired.
The above doesn't quite achieve what you want however. Alternatives that get you nearer the goal post would include using a "special" value that could be used to differentiate the user provided value, or to use a default. Ideally the value would be such that is it unlikely the user would use it; 0
comes to mind in this case. It still doesn't guarantee that the user would use 0
, but a 0
chunk size is unlikely in "reasonable" client code.
template<class T, size_t nChunkSize = 0>
class Holder
{
size_t m_nSize = 0;
size_t m_nChunkSize = nChunkSize == 0 ? 1000 : nChunkSize;
// ...
static_assert
can then be used to either allow the compilation of GetChunk
or not, based on the value of nChunkSize
- this works since nChunkSize
is known at compile time.
T* GetChunk(size_t nChunkIndex)
{
static_assert(nChunkSize != 0, "Method call invalid without client chunk size");
// ...
The disadvantage is that GetChunk
is still "visible" during development, but the compile will fail if it is invoked.
The closest you could get has been mentioned already, but repeated here for comparison; is to defer the implementation for the class to some BaseHolder
and then combine that with partial template specialisation to determine if the client code used a chunk size (a value for nChunkSize
) or not.
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)
{
// ...
}
};
The disadvantage of this approach is that multiple size_t
arguments could be provided, this can be controlled at compile time with a static_assert
; the documentation for the code should make this clear as well.
Upvotes: 0
Reputation: 13320
I will suggest to use two different classes: if they're expected to have different implementations, why stick with one single definition?
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(); }
};
Then, we define helper functions to create one class or the other:
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}; }
Finally, we can use it this way:
auto x = Holder<int, 200u>(5000u);
auto y = Holder<int>(500u);
x
is a Holder
1 wit the chunk feature and y
lacks of that feature and fails to compile the GetChunk
call, just because the underlying type lacks of that function.
See the live demo here.
ChunkHolder
, you can create a base class with the common implementation (operator[]
, ...
) or use different classes; it depends on your implementation needs.Upvotes: 1
Reputation: 41100
You could use a common base class with two derived classes: one that specializes for the scenario where a size_t
is provided, and another where one is not provided:
Base (Basically your current class):
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();
}
};
Defaulted (no way to call 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;
};
Nondefaulted (has GetChunk
method):
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;
}
};
Upvotes: 3
Reputation: 65620
If nChunkSize
was a type template parameter you could use a default tag and work based on that. Since it's a non-type parameter, you could use a flag value for the default, then correct it in the class definition:
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
}
This will make GetChunk
fail to compile if no default argument was passed. Of course, if you pass the max size_t
to Holder
then it'll silently get fixed up to 1000
, but presumably you aren't planning on passing values that high.
Upvotes: 2