tags:

views:

124

answers:

3

We use boost - so using that library should be fine.

But I've never managed to wrap my head around creating a set of templates which give you the right specialization for a whole class of data types, as opposed to specializing for a single data type (which I know how to do).

Let me go for an example to try to bring this down to earth. I want to have a set of classes which can be used as:

Initialized<T> t;

Where T is either a simple basic type, a PODS, or an array. It cannot be a class, for a class is expected to have its own constructor, and overwriting its raw memory is a terrible idea.

Initialized should basically memset(&t, 0, sizeof(t)); It makes it easier to ensure that runtime code is not different from debug code when dealing with legacy structs.

Initialized where SDT = simple data type, should simply create a struct which wrappers the underlying SDT and uses the compilers t() to generate the compiler defined default constructor for that type (it could amount to a memset as well, though it seems more elegant to simply result in t().

Here's a stab at it, using Initialized<> for PODs, and Initialised<> for SDTs:

// zeroed out PODS (not array)
// usage:  Initialized<RECT> r;
template <typename T>
struct Initialized : public T
{
    // ensure that we aren't trying to overwrite a non-trivial class
    BOOST_STATIC_ASSERT((boost::is_POD<T>::value));

    // publish our underlying data type
    typedef T DataType;

    // default (initialized) ctor
    Initialized() { Reset(); }

    // reset
    void Reset() { Zero((T&)(*this)); }

    // auto-conversion ctor
    template <typename OtherType> Initialized(const OtherType & t) : T(t) { }

    // auto-conversion assignment
    template <typename OtherType> Initialized<DataType> & operator = (const OtherType & t) { *this = t; }
};

And for SDTs:

// Initialised for simple data types - results in compiler generated default ctor
template <typename T>
struct Initialised
{
    // default valued construction
    Initialised() : m_value() { }

    // implicit valued construction (auto-conversion)
    template <typename U> Initialised(const U & rhs) : m_value(rhs) { }

    // assignment
    template <typename U> T & operator = (const U & rhs) { if ((void*)&m_value != (void*)&rhs) m_value = rhs; return *this; }

    // implicit conversion to the underlying type
    operator T & () { return m_value; }
    operator const T & () const { return m_value; }

    // the data
    T   m_value;
};

I have specialized Initialised for T*, to provide natural pointer behaviors. And I have an InitializedArray<> for arrays, which takes both the element type and array-size as template arguments. But again, I have to use template name to distinguish - I don't grok MPL well enough to provide a single template that results in the correct specialization at compile time all from a single name (Initialized<>, ideally).

I would love also to be able to provide an overloaded Initialized<typename T, T init_value> as well, so that for non-scalar values the user could define the default initialization value (or memset value)

I apologize for asking something that may take a bit of effort to answer. This seems to be a hurdle that I haven't been able to overcome in my own MPL reading on my own, but perhaps with some of your help I may be able to nail this functionality down!


Based on Uncle Ben's answer(s) below, I tried the following:

// containment implementation
template <typename T, bool bIsInheritable = false>
struct InitializedImpl
{
    // publish our underlying data type
    typedef T DataType;

    // auto-zero construction
    InitializedImpl() : m_value() { }

    // auto-conversion constructor
    template <typename U> InitializedImpl(const U & rhs) : m_value(rhs) { }

    // auto-conversion assignment
    template <typename U> T & operator = (const U & rhs) { if ((void*)&m_value != (void*)&rhs) m_value = rhs; return *this; }

    // implicit conversion to the underlying type
    operator T & () { return m_value; }
    operator const T & () const { return m_value; }

    // the data
    T   m_value;
};

// inheritance implementation
template <typename T>
struct InitializedImpl<T,true> : public T
{
    // publish our underlying data type
    typedef T DataType;

    // auto-zero ctor
    InitializedImpl() : T() { }

    // auto-conversion ctor
    template <typename OtherType> InitializedImpl(const OtherType & t) : T(t) { }

    // auto-conversion assignment
    template <typename OtherType> InitializedImpl<DataType> & operator = (const OtherType & t) { *this = t; }
};

// attempt to use type-traits to select the correct implementation for T
template <typename T>
struct Initialized : public InitializedImpl<T, boost::is_class<T>::value>
{
    // publish our underlying data type
    typedef T DataType;
};

And then tried a couple of usage tests.

int main()
{
    Initialized<int> i;
    ASSERT(i == 0);
    i = 9;  // <- ERROR
}

This results in an error: *binary '=' : no operator found which takes a right-hand operand of type 'InitializedImpl ' (or there is no acceptable conversion)

Whereas if I directly instantiate the correct base type (instead of a derived type):

int main()
{
    InitializedImpl<int,false> i;
    ASSERT(i == 0);
    i = 9;  // <- OK
}

Now I can use i as any old int. Which is what I want!

The exact same problems arise if I try to do the same for structs:

int main()
{
    Initialized<RECT> r;
    ASSERT(r.left == 0);  // <- it does let me access r's members correctly! :)

    RECT r1;
    r = r1;  // <- ERROR

    InitializedImpl<RECT,true> r2;
    r2 = r1; // OK
}

So, as you can see, I need some way to tell the compiler to promote an Initialized to act like a true T.

If C++ let me inherit from basic types, I could just use the inheritance technique and all would be well.

Or if I had some way to tell the compiler to extrapolate all methods in parent to child, so that anything valid on parent was valid on child, I'd be okay.

Or if I could use MPL or type-traits to typedef instead of inherit what I need, then there would be no child class, and no propagation issue.

Ideas?!...

A: 

I know it doesn't answer your question, but I thought POD structures were always zero-initialised anyway.

Autopulated
Default-initialization of PODs will translate to zero-initialization, but it doesn't happen in every case.
Dan Olson
Oh okay, fair enough.
Autopulated
Translate to - didn't realize that. But it still means that it has to be a member variable in order for the compiler to generate that default (zero) constructor. If I have a RECT, and delcare a RECT r; in a function body, it is NOT initialized. So it can be convenient and help make my code more robust to use an Initialized<RECT> r; which is zero'd for me.
Mordachai
+2  A: 

Initialized should basically memset(&t, 0, sizeof(t)); It makes it easier to ensure that runtime code is not different from debug code when dealing with legacy structs.

I don't think you should need memset, because you can zero-initialize PODs just as you can explicitly invoke the default constructor of non-PODs. (unless I'm terribly mistaken).

#include <cassert>

struct X {int a, b; };

template <typename T>
struct Initialized
{
    T t;

    // default (initialized) ctor
    Initialized(): t()  { }

};

template <typename T>
struct WithInheritance: public T
{
    // default (initialized) ctor
    WithInheritance(): T()  { }
};

int main()
{
    Initialized<X> a;
    assert(a.t.a == 0 && a.t.b == 0);

    //it would probably be more reasonable not to support arrays,
    //and use boost::array / std::tr1::array instead
    Initialized<int[2]> b;
    assert(b.t[0] == 0 && b.t[1] == 0);

    WithInheritance<X> c;
    assert(c.a == 0 && c.b == 0);
}

In your quest to determine the pod-ness of a type, you might also take into account this note from boost::is_pod reference:

Without some (as yet unspecified) help from the compiler, is_pod will never report that a class or struct is a POD; this is always safe, if possibly sub-optimal. Currently (May 2005) only MWCW 9 and Visual C++ 8 have the necessary compiler intrinsics.

(I think boost::type_traits are making it into the standard library in C++0x, and in such a case it would be reasonable to expect an is_pod that actually works.)


But if you want to specialize based on a condition, you can introduce a bool parameter. E.g something like this:

#include <limits>
#include <cstdio>

template <class T, bool b>
struct SignedUnsignedAux
{
    void foo() const { puts("unsigned"); }
};

template <class T>
struct SignedUnsignedAux<T, true>
{
    void foo() const { puts("signed"); }
};

//using a more reliable condition for an example
template <class T>
struct SignedUnsigned: SignedUnsignedAux<T, std::numeric_limits<T>::is_signed > {};

int main()
{
    SignedUnsigned<int> i;
    SignedUnsigned<unsigned char> uc;
    i.foo();
    uc.foo();
}

Here's also something that works sort of like you might be imagining (compiles at least with MinGW 4.4 and VC++ 2005 - the latter also nicely produces a warning that the array will be zero-initialized! :)).

This uses a default boolean argument which you probably shouldn't ever specify yourself.

#include <boost/type_traits.hpp>
#include <iostream>

template <class T, bool B = boost::is_scalar<T>::value>
struct Initialized
{
    T value;
    Initialized(const T& value = T()): value(value) {}
    operator T& () { return value; }
    operator const T& () const { return value; }
};

template <class T>
struct Initialized<T, false>: public T
{
    Initialized(const T& value = T()): T(value) {}
};

template <class T, size_t N>
struct Initialized<T[N], false>
{
    T array[N];
    Initialized(): array() {}
    operator T* () { return array; }
    operator const T* () const { return array; }
};

//some code to exercise it

struct X
{
    void foo() const { std::cout << "X::foo()" << '\n'; }
};

void print_array(const int* p, size_t size)
{
    for (size_t i = 0; i != size; ++i) {
        std::cout << p[i] <<  ' ';
    }
    std::cout << '\n';
}

template <class T>
void add_one(T* p, size_t size)
{
    for (size_t i = 0; i != size; ++i) {
        p[i] += T(1);
    }
}

int main()
{
    Initialized<int> a, b = 10;
    a = b + 20;
    std::cout << a << '\n';
    Initialized<X> x;
    x.foo();
    Initialized<int[10]> arr /*= {1, 2, 3, 4, 5}*/; //but array initializer will be unavailable
    arr[6] = 42;
    add_one<int>(arr, 10);  //but template type deduction fails
    print_array(arr, 10);
}

However, Initialized will probably never be as good as the real thing. One short-coming is shown in the test code: it can interfere with template type deduction. Also, for arrays you'll have a choice: if you want to zero-initialize it with the constructor, then you can't have non-default array initialization.

If the usage is that you are going to track down all uninitialized variables and wrap them into Initialized, I'm not quite sure why you won't just initialized them yourself.

Also, for tracking down uninitialized variables, perhaps compiler warnings can help a lot.

UncleBens
Excellent. It sounds like I can just use Initialized w/o worrying about whether its an array, struct, or simple type, other than pointer, if I simply provide the necessary operators for all of the above. I would use with-inheritance model for the natural member access semantics, but I want to also support simple types, which cannot be inherited from.
Mordachai
The main question I have remaining, is how might I use an integral value (perhaps an enum) for the specialization aspect, instead of a bool. Hence, if I could Initialized<T, some-integral-value-that-controls-the-partial-specialization-using-type-traits>, then I would perhaps be golden. E.g. I wouldn't want to necessarily provide array index operators except when the type is an array. Again, this is for legacy code, as a drop-in wrapper, rather than for newer, better code that would use vectors or somesuch anyway.
Mordachai
You can use any integer type besides bool. But you probably just want one specialization for built-in types and the other for anything that can be inherited from.
UncleBens
The trouble that I run into as soon as I try to implement your approach is that I cannot use an Initialized<T> as I would a T. e.g. Initialized<int> i; i = 5; *binary '=' : no operator found which takes a right-hand operand of type 'int'*. My Initialised<> template works for that sort of usage. But when abstracting it so that either an InitializedImpl<T,true> or InitializedImpl<T,false> is the supertype, the compiler seems to get lost in how to tread the Initialized<T> : InitailizedImpl<T,boost::is_class<T>::value> unless I skip and directly instantiate InitializedImpl<int,false> i;
Mordachai
Its like I need the compiler to allow me to generate the correct class based on usage, not a subclass of the correct base class on usage!
Mordachai
Added an example, how it might be done.
UncleBens
Thanks for all of your assistance, UB. I think perhaps this is the limits to what can be hoped for from the current generation of C++ and templates, and the fact of the matter is that there are limitations with this approach. A Wrappered<T> is never quite a T. It would be really nice if the next version of C++ gave us a language-level mechanic for such a thing (exporting the T'ness out of the Wrappered<T>), but until such a time, this is as good as I think it gets. Thanks. :)
Mordachai
A: 

Since I was able to use UncleBen's answers to create a comprehensive solution (as good as I think it gets at this point in C++), I wanted to share it, below:

Feel free to use it, but I make no guarantees whatsoever about its worthiness for any use whatsoever, etc., etc., be an adult and take responsibility for your own damn actions, blah, blah:

//////////////////////////////////////////////////////////////
// Raw Memory Initialization Helpers
//
//  Provides:
//      Zero(x) where x is any type, and is completely overwritten by null bytes (0).
//      Initialized<T> x; where T is any legacy type, and it is completely null'd before use.
//
// History:
//
//  User UncleBen of stackoverflow.com and I tried to come up with 
//  an improved, integrated approach to Initialized<>
//  http://stackoverflow.com/questions/2238197/how-do-i-specialize-a-templated-class-for-data-type-classification
//
//  In the end, there are simply some limitations to using this
//  approach, which makes it... limited.
//
//  For the time being, I have integrated them as best I can
//  However, it is best to simply use this feature
//  for legacy structs and not much else.
//
//  So I recommend stacked based usage for legacy structs in particular:
//      Initialized<BITMAP> bm;
//
//  And perhaps some very limited use legacy arrays:
//      Initialized<TCHAR[MAX_PATH]> filename;
//
//  But I would discourage their use for member variables:
//      Initialized<size_t> m_cbLength;
//  ...as this can defeat template type deduction for such types 
//  (its not a size_t, but an Initialized<size_t> - different types!)
//
//////////////////////////////////////////////////////////////

#pragma once

// boost
#include <boost/static_assert.hpp>
#include <boost/type_traits.hpp>

// zero the memory space of a given PODS or native array
template <typename T>
void Zero(T & object, int zero_value = 0)
{
    // ensure that we aren't trying to overwrite a non-trivial class
    BOOST_STATIC_ASSERT((boost::is_POD<T>::value));

    // make zeroing out a raw pointer illegal
    BOOST_STATIC_ASSERT(!(boost::is_pointer<T>::value));

    ::memset(&object, zero_value, sizeof(object));
}

// version for simple arrays
template <typename T, size_t N>
void Zero(T (&object)[N], int zero_value = 0)
{
    // ensure that we aren't trying to overwrite a non-trivial class
    BOOST_STATIC_ASSERT((boost::is_POD<T>::value));

    ::memset(&object, zero_value, sizeof(object));
}

// version for dynamically allocated memory
template <typename T>
void Zero(T * object, size_t size, int zero_value = 0)
{
    // ensure that we aren't trying to overwrite a non-trivial class
    BOOST_STATIC_ASSERT((boost::is_POD<T>::value));

    ::memset(object, zero_value, size);
}

//////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////


//////////////////////////////////////////////////////////////////////////
// Initialized for non-inheritable types
// usage: Initialized<int> i;
template <typename T, bool SCALAR = boost::is_scalar<T>::value>
struct Initialized
{
    // ensure that we aren't trying to overwrite a non-trivial class
    BOOST_STATIC_ASSERT((boost::is_scalar<T>::value));

    // the data
    T   m_value;

    // default valued construction
    Initialized() : m_value() { }

    // implicit valued construction (auto-conversion)
    template <typename U> Initialized(const U & rhs) : m_value(rhs) { }

    // assignment
    template <typename U> T & operator = (const U & rhs) { if ((void*)&m_value != (void*)&rhs) m_value = rhs; return *this; }

    // implicit conversion to the underlying type
    operator T & () { return m_value; }
    operator const T & () const { return m_value; }

    // zero method for this type
    void _zero() { m_value = T(); }
};

//////////////////////////////////////////////////////////////////////////
// Initialized for inheritable types (e.g. structs)
// usage:  Initialized<RECT> r;
template <typename T>
struct Initialized<T, false> : public T
{
    // ensure that we aren't trying to overwrite a non-trivial class
    BOOST_STATIC_ASSERT((boost::is_POD<T>::value));

    // default ctor
    Initialized() : T() {  }

    // auto-conversion ctor
    template <typename OtherType> Initialized(const OtherType & value) : T(value) { }

    // auto-conversion assignment
    template <typename OtherType> Initialized & operator = (const OtherType & value) { *this = value; }

    // zero method for this type
    void _zero() { Zero((T&)(*this)); }
};

//////////////////////////////////////////////////////////////////////////
// Initialized arrays of simple types
// usage: Initialized<char, MAXFILENAME> szFilename;
template <typename T, size_t N>
struct Initialized<T[N],false>
{
    // ensure that we aren't trying to overwrite a non-trivial class
    BOOST_STATIC_ASSERT((boost::is_POD<T>::value));

    // internal data
    T m_array[N];

    // default ctor
    //Initialized() : m_array() { } // Generates a warning about new behavior.  Its okay, but might as well not produce a warning.
    Initialized() { Zero(m_array); }

    // array access
    operator T * () { return m_array; }
    operator const T * () const { return m_array; }

    // NOTE: All of the following techniques leads to ambiguity.
    //       Sadly, allowing the type to convert to ArrayType&, which IMO should
    //       make it fully "the same as it was without this wrapper" instead causes
    //       massive confusion for the compiler (it doesn't understand IA + offset, IA[offset], etc.)
    //       So in the end, the only thing that truly gives the most bang for the buck is T * conversion.
    //       This means that we cannot really use this for <char> very well, but that's a fairly small loss
    //       (there are lots of ways of handling character strings already)

    //  // automatic conversions
    //  operator ArrayType& () { return m_array; }
    //  operator const ArrayType& () const { return m_array; }
    // 
    //  T * operator + (long offset) { return m_array + offset; }
    //  const T * operator + (long offset) const { return m_array + offset; }
    // 
    //  T & operator [] (long offset) { return m_array[offset]; }
    //  const T & operator [] (long offset) const { return m_array[offset]; }

    // metadata
    size_t GetCapacity() const { return N; }

    // zero method for this type
    void _zero() { Zero(m_array); }
};

//////////////////////////////////////////////////////////////////////////
// Initialized for pointers to simple types
// usage: Initialized<char*> p;
// Please use a real smart pointer (such as std::auto_ptr or boost::shared_ptr)
//  instead of this template whenever possible.  This is really a stop-gap for legacy
//  code, not a comprehensive solution.
template <typename T>
struct Initialized<T*, true>
{
    // the pointer
    T * m_pointer;

    // default valued construction
    Initialized() : m_pointer(NULL) { }

    // valued construction (auto-conversion)
    template <typename U> Initialized(const U * rhs) : m_pointer(rhs) { }

    // assignment
    template <typename U> T * & operator = (U * rhs) { if (m_pointer != rhs) m_pointer = rhs; return *this; }
    template <typename U> T * & operator = (const U * rhs) { if (m_pointer != rhs) m_pointer = rhs; return *this; }

    // implicit conversion to underlying type
    operator T * & () { return m_pointer; }
    operator const T * & () const { return m_pointer; }

    // pointer semantics
    const T * operator -> () const { return m_pointer; }
    T * operator -> () { return m_pointer; }
    const T & operator * () const { return *m_pointer; }
    T & operator * () { return *m_pointer; }

    // allow null assignment
private:
    class Dummy {};
public:
    // amazingly, this appears to work.  The compiler finds that Initialized<T*> p = NULL to match the following definition
    T * & operator = (Dummy * value) { m_pointer = NULL; ASSERT(value == NULL); return *this; }

    // zero method for this type
    void _zero() { m_pointer = NULL; }
};

//////////////////////////////////////////////////////////////////////////
// Uninitialized<T> requires that you explicitly initialize it when you delcare it (or in the owner object's ctor)
//  it has no default ctor - so you *must* supply an initial value.
template <typename T>
struct Uninitialized
{
    // valued initialization
    Uninitialized(T initial_value) : m_value(initial_value) { }

    // valued initialization from convertible types
    template <typename U> Uninitialized(const U & initial_value) : m_value(initial_value) { }

    // assignment
    template <typename U> T & operator = (const U & rhs) { if (&m_value != &rhs) m_value = rhs; return *this; }

    // implicit conversion to underlying type
    operator T & () { return m_value; }
    operator const T & () const { return m_value; }

    // the data
    T   m_value;
};

//////////////////////////////////////////////////////////////////////////
// Zero() overload for Initialized<>
//////////////////////////////////////////////////////////////////////////

// version for Initialized<T>
template <typename T, bool B>
void Zero(Initialized<T,B> & object)
{
    object._zero();
}
Mordachai