40

I have a C++03 application where std::vector<T> types are used throughout as temporary buffers. As such, they often get resized using std::vector<T>::resize() to ensure they are large enough to hold the required data before use. The C++03 prototype for this function is actually:

void resize(size_type n, value_type val = value_type());

So in actuality when calling resize(), the vector is enlarged by adding the appropriate number of copies of val. Often, however, I just need to know that the vector is large enough to hold the data I need; I don't need it initialized with any value. Copy-constructing the new values is just a waste of time.

C++11 comes to the rescue (I thought): in its specification, it splits resize() into two overloads:

void resize(size_type n); // value initialization
void resize(size_type n, const value_type &val); // initialization via copy

This fits nicely with the philosophy of C++: only pay for what you want. As I noted, though, my application can't use C++11, so I was happy when I came across the Boost.Container library, which indicates support for this functionality in its documentation. Specifically, boost::container::vector<T> actually has three overloads of resize():

void resize(size_type n); // value initialization
void resize(size_type n, default_init_t); // default initialization
void resize(size_type n, const value_type &val); // initialization via copy

In order to verify that I understood everything, I whipped up a quick test to verify the behavior of C++11 std::vector<T> and boost::container::vector<T>:

#include <boost/container/vector.hpp>
#include <iostream>
#include <vector>

using namespace std;
namespace bc = boost::container;

template <typename VecType>
void init_vec(VecType &v)
{
    // fill v with values [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    for (size_t i = 0; i < 10; ++i) v.push_back(i);
    // chop off the end of v, which now should be [1, 2, 3, 4, 5], but the other 5 values
    // should remain in memory
    v.resize(5);
}

template <typename VecType>
void print_vec(const char *label, VecType &v)
{
    cout << label << ": ";
    for (size_t i = 0; i < v.size(); ++i)
    {
        cout << v[i] << ' ';
    }
    cout << endl;
}

int main()
{
    // instantiate a vector of each type that we're going to test
    std::vector<int> std_vec;
    bc::vector<int> boost_vec;
    bc::vector<int> boost_vec_default;

    // fill each vector in the same way
    init_vec(std_vec);
    init_vec(boost_vec);
    init_vec(boost_vec_default);

    // now resize each vector to 10 elements in ways that *should* avoid reinitializing the new elements
    std_vec.resize(10);
    boost_vec.resize(10);
    boost_vec_default.resize(10, bc::default_init);

    // print each one out
    print_vec("std", std_vec);
    print_vec("boost", boost_vec);
    print_vec("boost w/default", boost_vec_default);    
}

Compiling this with g++ 4.8.1 in C++03 mode as follows:

g++ vectest.cc
./a.out

yields the following output:

std: 0 1 2 3 4 0 0 0 0 0 
boost: 0 1 2 3 4 0 0 0 0 0 
boost w/default: 0 1 2 3 4 5 6 7 8 9

This isn't too surprising. I expect the C++03 std::vector<T> to initialize the final 5 elements with zeros. I can even convince myself why boost::container::vector<T> is doing the same (I would assume it emulates C++03 behavior in C++03 mode). I only got the effect that I wanted when I specifically ask for default initialization. However, when I rebuilt in C++11 mode as follows:

g++ vectest.cc -std=c++11
./a.out

I get these results:

std: 0 1 2 3 4 0 0 0 0 0 
boost: 0 1 2 3 4 0 0 0 0 0 
boost w/default: 0 1 2 3 4 5 6 7 8 9

Exactly the same! Which leads to my question:

Am I wrong in thinking that I should see the same results from each of the three tests in this case? This seems to indicate that the std::vector<T> interface change hasn't really had any effect, as the 5 elements added in the final call to resize() still get initialized with zeros in the first two cases.

Jason R
  • 11,159
  • 6
  • 50
  • 81
  • possible duplicate of [Value-Initialized Objects in C++11 and std::vector constructor](http://stackoverflow.com/questions/15097783/value-initialized-objects-in-c11-and-stdvector-constructor) – Nevin Jan 09 '14 at 18:52

7 Answers7

83

Not an answer, but a lengthy addendum to Howard's: I use an allocator adapter that basically works the same as Howard's allocator, but is safer since

  1. it only interposes on value-initialization and not all initializations,
  2. it correctly default-initializes.
// Allocator adaptor that interposes construct() calls to
// convert value initialization into default initialization.
template <typename T, typename A=std::allocator<T>>
class default_init_allocator : public A {
  typedef std::allocator_traits<A> a_t;
public:
  template <typename U> struct rebind {
    using other =
      default_init_allocator<
        U, typename a_t::template rebind_alloc<U>
      >;
  };

  using A::A;

  template <typename U>
  void construct(U* ptr)
    noexcept(std::is_nothrow_default_constructible<U>::value) {
    ::new(static_cast<void*>(ptr)) U;
  }
  template <typename U, typename...Args>
  void construct(U* ptr, Args&&... args) {
    a_t::construct(static_cast<A&>(*this),
                   ptr, std::forward<Args>(args)...);
  }
};
Community
  • 1
  • 1
Casey
  • 41,449
  • 7
  • 95
  • 125
  • 1
    @HowardHinnant Thanks - I stole this allocator the last time you posted it and liked it enough to armor it a bit against UB. – Casey Jan 09 '14 at 19:07
  • 10
    PS: I substituted Casey's allocator and got the same results as my answer shows. Casey's allocator should have the same performance as mine, and is safer. – Howard Hinnant Jan 09 '14 at 19:08
  • 22
    http://en.cppreference.com/w/cpp/container/vector/resize now links here. That's cool :) – Peter Cordes Sep 20 '15 at 05:02
  • What does the `using A::A;` do? – Roland Schulz Apr 18 '17 at 01:52
  • @RolandSchulz `using A::A;` inherits constructors from the adapted allocator type `A` so that `default_init_allocator` is constructible from any set of arguments that can be used to construct an `A`. – Casey Apr 24 '17 at 14:43
  • In C++17, `construct` has been deprecated. Does this change? I believe the idea is that users of an allocator should use `std::allocator_traits::construct`, but it's not clear how to create an allocator, to use with a container, without defining `std::allocator::construct`. – TrentP Mar 15 '18 at 01:44
  • 5
    @TrentP `std::allocator_traits::construct(Alloc& a, U* u, Args&&... args)` delegates to `a.construct(u, args...)` if it's valid, and otherwise is equivalent to `::new(u) U(args...)`. Since callers are expected to use `allocator_traits`, `std::allocator` doesn't need to implement a member `construct` which is equivalent to the fallback - hence it is deprecated. The adaptor implemented here uses `allocator_traits` instead of calling `construct` directly on the underlying allocator, so it continues to work fine even when adapting a `std::allocator` that no longer implements `construct`. – Casey Mar 20 '18 at 15:47
  • Good news @Casey. I was wondering about that myself. I was worried when I saw that it was deprecated. – Adrian Apr 27 '19 at 17:04
  • Wouldn't `std::allocator_traits>::construct(default_init_allocator& a, T* p, Args&&... args)` be transformed by the compiler directly to `std::construct_at(p, std::forward(args)...)` for nonzero number of `args` in the absence of the variadic `construct` overload in this answer? Is there a benefit to use a template on type parameter `U` to define the default-initializing constructor or is it equivalent to have `void construct(T* ptr)`? – ArborealAnole Aug 06 '21 at 10:10
  • In this case, it is better to use `::new` or just `new`? – Giovanni Cerretani Mar 02 '22 at 07:55
  • this doesn't work with PMR containers – ggg May 08 '22 at 15:12
  • Fairly hilariously I have observed the following regarding this fantastic comment. When used with `std::vector` it worked great. With `boost::container::vector` it... just didn't. That's the bad news (I have not had time to look into why exactly). The good news is it has a `resize(size_t, default_init_t)` which does what one wants anyway. – RedRedSuit Oct 12 '22 at 23:21
31

There is a small functional difference with the C++11 resize signatures, but your test will not expose it. Consider this similar test:

#include <iostream>
#include <vector>

struct X
{
    X() {std::cout << "X()\n";}
    X(const X&) {std::cout << "X(const X&)\n";}
};

int
main()
{
    std::vector<X> v;
    v.resize(5);
}

Under C++03 this prints:

X()
X(const X&)
X(const X&)
X(const X&)
X(const X&)
X(const X&)

But under C++11 it prints:

X()
X()
X()
X()
X()

The motivation for this change is to better support non-copyable (move-only) types in vector. Most of the time, including in your case, this change makes no difference.

There is a way to accomplish what you want in C++11 with the use of a custom allocator (which your compiler may or may not yet support):

#include <iostream>
#include <vector>

using namespace std;

template <class T>
class no_init_alloc
    : public std::allocator<T>
{
public:
    using std::allocator<T>::allocator;

    template <class U, class... Args> void construct(U*, Args&&...) {}
};


template <typename VecType>
void init_vec(VecType &v)
{
    // fill v with values [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    v.resize(10);
    for (size_t i = 0; i < 10; ++i) v[i] = i;  // Note this change!!!
    // chop off the end of v, which now should be [1, 2, 3, 4, 5], but the other 5 values
    // should remain in memory
    v.resize(5);
}

template <typename VecType>
void print_vec(const char *label, VecType &v)
{
    cout << label << ": ";
    for (size_t i = 0; i < v.size(); ++i)
    {
        cout << v[i] << ' ';
    }
    cout << endl;
}

int
main()
{
    std::vector<int, no_init_alloc<int>> std_vec;
    init_vec(std_vec);
    std_vec.resize(10);
    print_vec("std", std_vec);
}

Which should output:

std: 0 1 2 3 4 5 6 7 8 9 

The no_init_alloc simply refuses to do any initialization, which is fine for int, leaving it with an unspecified value. I had to change your init_vec to use assignment to initialize instead of using construction though. So this can be dangerous / confusing if you are not careful. However it does avoid doing unnecessary initialization.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • Wouldn't it be safer and just as fast if you said `using std::allocator::construct` and dropped the `Args` from your `construct`? Ie. only intercept calls to the default constructor? – Marc Mutz - mmutz Feb 24 '17 at 09:37
  • @MarcMutz-mmutz: Casey's allocator is the way to go on this one. It addresses the safety issue you speak of. – Howard Hinnant Feb 24 '17 at 15:53
  • Any chance for a c++-17-compliant version, given that std::allocator::construct() is deprecated in c++17? – FaulerHase Apr 14 '18 at 08:05
  • 1
    `no_init_alloc::construct` does not rely on the existence of `std::allocator::construct()`. And neither does Casey's `default_init_allocator::construct`. So it's all good to go. Note that _only_ `std::allocator::construct()` is deprecated and _not_ `allocator_traits::construct`. That is, `construct` is still an _optional_ requirement for allocators, and `allocator_traits` still supplies the default `construct` should the allocator choose not to override that default. The only difference in behavior is that you should no longer call `construct` as a member of `std::allocator`. – Howard Hinnant Apr 14 '18 at 15:33
  • I've tested this with gcc 7.3.1 (on Fedora 27) and it doesn't work as expected - I get this output: `0 1 2 3 4 0 0 0 0 0`. When I play around with it in GCC explorer with different compilers I see memset still being invoked. In contrast to that, Casey's adaptor works fine. Perhaps has something to do with allocator users like std::vector calling `Allocator::rebind::other` all the time - even when U is the same as T. – maxschlepzig Sep 25 '18 at 07:30
  • will it not cause undefined-behavior once the vector reallocates and the element was not yet assigned? Because then, it will read uninitialized data. If T is `int` for example. – Johannes Schaub - litb Jun 02 '21 at 12:23
  • I suspect that real-world implementations optimize reallocation to use `memcpy` for non-class types, so it might not be UB anymore then (?). But it's still worth noting. – Johannes Schaub - litb Jun 02 '21 at 12:32
  • this doesn't work with PMR containers – ggg May 08 '22 at 15:11
3

Uninitialized values

You may have initialized value by creating the appropriate class. As the following:

class uninitializedInt
{
public:
    uninitializedInt() {};
    uninitializedInt(int i) : i(i) {};

    operator int () const { return i; }

private:
    int i;
};

The output is identical to "boost w/default".

Or create a custom allocator with construct and destroy as nop.

Splitting resize prototype

If void std::vector<T>::resize(size_type n) does what void bc::vector<T>::resize(size_type n, default_init_t) does, then lot of old valid code would break...


The splits of resize() allows to resize vector of 'move only' classes as the following:

class moveOnlyInt
{
public:
    moveOnlyInt() = default;
    moveOnlyInt(int i) : i(i) {};

    moveOnlyInt(const moveOnlyInt&) = delete;
    moveOnlyInt(moveOnlyInt&&) = default;
    moveOnlyInt& operator=(const moveOnlyInt&) = delete;
    moveOnlyInt& operator=(moveOnlyInt&&) = default;

    operator int () const { return i; }
private:
    int i;
};
Jarod42
  • 203,559
  • 14
  • 181
  • 302
2

So in actuality when calling resize(), the vector is enlarged by adding the appropriate number of copies of val. Often, however, I just need to know that the vector is large enough to hold the data I need; I don't need it initialized with any value. Copy-constructing the new values is just a waste of time.

No, not really. Having a container of elements that are not actually constructed does not make sense. I'm not sure what you expected to see other than zeroes. Unspecified/uninitialised elements? That's not what value-initialisation means.

If you need N elements, then you should have N properly-constructed elements, and that is what std::vector::resize does. Value-initialisation will zero-initialise an object with no default constructor to invoke, so really it's the opposite of what you seem to want, which is less safety and initialisation rather than more.

I suggest that what you're really after is std::vector::reserve.

This seems to indicate that the std::vector<T> interface change hasn't really had any effect

It certainly has an effect, just not the one you're looking for. The new resize overload is for convenience so that you don't have to construct your own temporary when default- or even value-initialisation is all you need. It isn't a fundamental change to how containers work, which is that they always hold valid instances of types.

Valid but in an unspecified state if you move from them!

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • I should have been more explicit, but I'm restricting myself to the cases where `T` is POD and is therefore trivially constructible. I'm probably using the wrong terms with respect to standardese, but yes, I was looking for similar behavior to `reserve()`, but using that just doesn't feel right. – Jason R Jan 09 '14 at 18:38
  • @JasonR: It sounds precisely like you're looking for `reserve` but the reason it doesn't feel right is that you're taking a C approach to programming C++. – Lightness Races in Orbit Jan 09 '14 at 18:38
  • @JasonR, "just doesn't feel right" is not a reason for not using it. Try again. – Shoe Jan 09 '14 at 18:40
  • 1
    @Jefffrey: Isn't it [undefined behavior](http://stackoverflow.com/questions/7689406/resizing-a-c-stdvectorchar-without-initializing-data) to access the elements of a `vector` past its `size()`? It seems that the Boost container version of resize with default initialization is what I want. – Jason R Jan 09 '14 at 18:44
  • @JasonR: Why would you do that? Why would you attempt to read elements with unspecified values? Add them when you need them and can give them a value. There's just something fundamentally wrong about how you're trying to use this container. Frankly it sounds like a bit of an XY problem to me... – Lightness Races in Orbit Jan 09 '14 at 18:45
  • 2
    @LightnessRacesinOrbit: Of course I wouldn't try to read them. I was referencing some earlier answers that indicated that accessing the `reserve()`-ed elements (even for write) was undefined behavior. I'm not sure why my approach would seem so strange. For example, I have a buffer that currently holds 5000 `char`s. I'd like to resize it to hold 1000000 `char`s without initializing all those `char`s to zeros first (if the `vector` has already been that large before, the memory is already there). I work on HPC applications where the time/cache pollution to fill that memory is not negligible. – Jason R Jan 09 '14 at 18:51
  • @JasonR: And what value will you be giving those `char`s shortly before really starting to use them? Add the `char`s _at that time_ when you know the real value to give them, rather than this strange "I want to have phantom elements that I'm not using yet" notion that is precisely the use case for `.reserve`. You reserve the memory (nobody said you should read or write from it), so that later adding your actual, value-full elements is cheap as chips. No need for all this hackery. I do sort of see why you might not want zero-initialisation for a POD but you don't need this hackery. – Lightness Races in Orbit Jan 09 '14 at 18:53
  • 1
    @LightnessRacesinOrbit: Consider the case where I'm passing that vector to an API that fills it with a block of data. Yes, that is perhaps a C-like API, but it is a very common pattern, even in C++ libraries like Boost.ASIO. – Jason R Jan 09 '14 at 18:55
  • @JasonR: Then a C++ vector is not the appropriate container for the job. Yes, if there were a `std::vector::resize` that performed _no_ initialisation on new elements, you would be able to do it. But this is simply not the semantics of a high-level container construct. You may use the Boost container to do the job. – Lightness Races in Orbit Jan 09 '14 at 18:55
  • 3
    @LightnessRacesinOrbit - I believe that vector::reserve() doesn't alter the value returned from vector::size(), which means that attempting to access an element of the a vector extended via reserve() beyond the original size of the vector via the array operator or get() method will likely produce an exception. – Bukes Jan 09 '14 at 19:07
  • 2
    @Bukes: Not "likely": with `at()`, definitely; with `[]`, definitely not. But yes the key is that there is a distinction between the conceptual size of the container, and the quantity of memory it has allocated internally in order to hold data. Any elements that are _conceptually_ "in" the container must have been initialised, and that is where the OP is tripping up. I thought he might have been able to use reservation in the interim to get around his problem, because he hadn't actually told us what the problem was. :) If it were an allocate-up-front requirement, `reserve` would be right. – Lightness Races in Orbit Jan 09 '14 at 19:20
  • 2
    @LightnessRacesinOrbit - true. For what it is worth, recent vintages of the Microsoft implementations will raise an exception when attempting array[] access on a vector element that is out of bounds if checked iterators are enabled (which is the default) See: http://msdn.microsoft.com/en-us/library/aa985965.aspx – Bukes Jan 09 '14 at 19:34
  • @Bukes: That's disgraceful :( – Lightness Races in Orbit Jan 09 '14 at 19:37
2

Value initialization of int yields 0.

Default initialization of int doesn't initialize the value at all - it just retains whatever was in memory.

Either the memory allocated by resize(10) wasn't released by resize(5), or the same memory block was reused. Either way you ended up with the prior contents left over.

Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
1

if you want to use a vector with the standard allocator, doesn't this work in C++11??

    namespace{
       struct Uninitialised {};

       template<typename T>
       template<typename U>
       std::allocator<T>::construct(U* , Uninitialised&&)
       {
          /*do nothing*/
       }; 
    }

   template<typename T>
   void resize_uninitialised(std::vector<T>& vec, 
                             std::vector<T>::size_type size)
   {
        const Uninitialised* p = nullptr;
        auto cur_size = vec.size();

        if(size <= cur_size)
          return;

        vec.reserve(size);

        //this should optimise to  vec.m_size += (size - cur_size);
        //one cannot help thinking there  must be simpler ways to do that. 
        vec.insert(vec.end(), p, p + (size - cur_size));
   };
R Brussee
  • 11
  • 1
1

A small note regarding Caseys answer:

As Casey notes under 1., above code only interposes on value-initialization. I don't know whether this entails, the following - but at least for me, it was not obvious.

Above code only avoids initialization of Plain Old Datatype [POD]-ed T in std::vector<T, default_init_allocator<T>>.

Whether this actually avoids runtime overhead however currently I don't know. Maybe someone else can answer that for me.

In case anyone wants to test this, I will append the code I used for actually testing this below. I should probably add that I used set(CMAKE_CXX_STANDARD 11) and a MinGW 8.1.0 64-bit C++ compiler.

    // ----------------------------------------------------

#include <iostream>
#include <memory>
#include <vector>

// ----------------------------------------------------

// forward declarations
class Blub;
std::ostream & operator<<(std::ostream &os, Blub const & blub);


class Blub
{
    static unsigned instanceCounter_;

 public:
    static unsigned constexpr NO_SOURCE = -1;

    // default constructor
    Blub()
        : instance(instanceCounter_)
        , instanceSource(NO_SOURCE)
    {
        std::cout << "default constructor: " << *this << std::endl;
        ++instanceCounter_;
    }

    // destructor
    ~Blub()
    {
        --instanceCounter_;
        std::cout << "destructor: " << *this << std::endl;
    }

    // copy constructor
    Blub(Blub const &other)
        : instance(instanceCounter_)
        , instanceSource(other.instance)
    {
        std::cout << "copy constructor: " << *this << std::endl;
        ++instanceCounter_;
    }

    // move constructor
    Blub(Blub &&other)
        : instance(std::move(other.instance))
        , instanceSource(std::move(other.instanceSource))
    {
        std::cout << "move constructor: " << *this  << std::endl;
    }

    // copy assignment
    Blub & operator=(Blub const &other)
    {
        instanceSource = other.instance;
        std::cout << "copy assignment: " << *this << std::endl;
        return (*this);
    }

    // move assignment
    Blub & operator=(Blub &&other)
    {
        instance = std::move(other.instance);
        instanceSource = std::move(other.instanceSource);
        std::cout << "move assignment: " << *this << std::endl;
        return (*this);
    }

    unsigned instance;
    unsigned instanceSource;
};

unsigned Blub::instanceCounter_;

std::ostream & operator<<(std::ostream &os, Blub const & blub)
{
    os << "Blub " << blub.instance;
    if (Blub::NO_SOURCE != blub.instanceSource)
    {
        os << " [from " << blub.instanceSource << "]";
    }
    return os;
}

// ----------------------------------------------------

// Allocator adaptor that interposes construct() calls to
// convert value initialization into default initialization.
template <typename T, typename A = std::allocator<T>>
class default_init_allocator : public A
{
    typedef std::allocator_traits<A> a_t;

public:

    template <typename U> struct rebind
    {
        using other = default_init_allocator<U, typename a_t::template rebind_alloc<U>>;
    };

    using A::A;

    template <typename U>
    void construct(U* ptr) noexcept(std::is_nothrow_default_constructible<U>::value)
    {
        ::new(static_cast<void*>(ptr)) U;
    }

    template <typename U, typename...Args>
    void construct(U* ptr, Args&&... args)
    {
        a_t::construct(static_cast<A&>(*this),
                       ptr, std::forward<Args>(args)...);
    }
};

// ----------------------------------------------------

template <typename VecType>
void print_vec(const char *label, VecType &v)
{
    std::cout << label << ": ";
    for (size_t i = 0; i < v.size(); ++i)
    {
        std::cout << v[i] << " ";
    }
    std::cout << std::endl;
}

// ----------------------------------------------------

int main()
{
    {
        std::cout << "POD:" << std::endl;

        std::vector<int, default_init_allocator<int>> vec(10);
        // fill with values
        for (size_t i = 0; i < 10; ++i)
        {
            vec[i] = i;
        }
        print_vec("initialized", vec);

        vec.resize(5);
        // this should not change value 5 to 9 in memory
        vec.resize(10);
        print_vec("resized", vec);
    }

    std::cout << std::endl;

    {
        std::cout << "C++ class:" << std::endl;
        std::vector<Blub, default_init_allocator<Blub>> vec(10);
        print_vec("initialized", vec);

        vec.resize(5);
        vec.resize(10);
        print_vec("resized", vec);
    }
}

// ----------------------------------------------------

The output I got from this was:

POD:
initialized: 0 1 2 3 4 5 6 7 8 9 
resized: 0 1 2 3 4 5 6 7 8 9 

C++ class:
default constructor: Blub 0
default constructor: Blub 1
default constructor: Blub 2
default constructor: Blub 3
default constructor: Blub 4
default constructor: Blub 5
default constructor: Blub 6
default constructor: Blub 7
default constructor: Blub 8
default constructor: Blub 9
initialized: Blub 0 Blub 1 Blub 2 Blub 3 Blub 4 Blub 5 Blub 6 Blub 7 Blub 8 Blub 9 
destructor: Blub 5
destructor: Blub 6
destructor: Blub 7
destructor: Blub 8
destructor: Blub 9
default constructor: Blub 5
default constructor: Blub 6
default constructor: Blub 7
default constructor: Blub 8
default constructor: Blub 9
resized: Blub 0 Blub 1 Blub 2 Blub 3 Blub 4 Blub 5 Blub 6 Blub 7 Blub 8 Blub 9 
destructor: Blub 0
destructor: Blub 1
destructor: Blub 2
destructor: Blub 3
destructor: Blub 4
destructor: Blub 5
destructor: Blub 6
destructor: Blub 7
destructor: Blub 8
destructor: Blub 9

PS: This full-fledged answer only came to be, because I am as of yet not allowed to make comments. Otherwise I probably would have only asked Casey to extend the answer above with a note, that this essentially only works for PODs.