10

Take from here (which is quite old):

It's also important that the type used for the allocator template parameter and the type used as the element type in a standard container agree. For instance:

std::list<int, std::allocator<long> >                    // Wrong!

won't work.

Question

Is the above statement correct (or was it ever correct)? Any tests I have done seem to work fine no matter what I put for T in std::allocator. For example, std::vector<int, std::allocator<std::string>> compiled and worked fine pushing back and erasing elements, etc. (From what I understand std::allocator<std::string>::rebind<int>::other is the magic that makes this work).

Jesse Good
  • 50,901
  • 14
  • 124
  • 166
  • check the size of the data structure when you use different types – aaronman Jul 30 '13 at 21:09
  • @aaronman: The size of `std::list` might or not differ when different data types are used for either the type or the allocator. That won't really tell you anything – David Rodríguez - dribeas Jul 30 '13 at 21:11
  • @DavidRodríguez-dribeas in the case of list you would check the size of one node if thats even possible – aaronman Jul 30 '13 at 21:12
  • @aaronman: what do you think you could make up from the size of the data structure or the size of the node? – David Rodríguez - dribeas Jul 30 '13 at 21:20
  • @DavidRodríguez-dribeas I thought that if the allocator is just allocating the wrong size you might be able to find out by checking the overall size of the data structure, just because you have the wrong size of a data structure the insertions may not cause an error depending how the insert is written – aaronman Jul 30 '13 at 21:23
  • Short answer to the question: the statement is **correct** the type must match. Long answer is long and convoluted... trying to figure out how to write it up in something that does not take too long. Your implementation (maybe gcc? I just checked the implementation of gcc and does this) might be rebinding the allocator instead of using the allocator that was received. – David Rodríguez - dribeas Jul 30 '13 at 21:31
  • @aaronman: You cannot know how much data has been dynamically allocated. – David Rodríguez - dribeas Jul 30 '13 at 21:33
  • @DavidRodríguez-dribeas is data in the std containers always dynamically allocated, I think it depends on the data structure – aaronman Jul 30 '13 at 21:34
  • @aaronman Storage is always dynamically allocated for all containers except `std::array` and (sometimes) `std::dynarray` – Praetorian Jul 30 '13 at 21:48
  • @Praetorian thanks I guess you could overload new if you wanted to figure out the size than – aaronman Jul 30 '13 at 21:50
  • @DavidRodríguez-dribeas: `might be rebinding the allocator instead of using the allocator that was received` Yes, that seems to be the case. I was wondering if this was standard conforming. – Jesse Good Jul 30 '13 at 21:58

2 Answers2

7

I'm adding an answer here to clarify the difference between ill-formed and undefined behavior.

[intro.compliance]/p1:

The set of diagnosable rules consists of all syntactic and semantic rules in this International Standard except for those rules containing an explicit notation that “no diagnostic is required” or which are described as resulting in “undefined behavior.”

[defns.ill.formed]:

program that is not well formed

[defns.well.formed]

C++ program constructed according to the syntax rules, diagnosable semantic rules, and the One Definition Rule (3.2).

In English: an ill-formed program shall have a diagnostic associated with it. Undefined behavior can do anything:

  1. It can compile and execute as you intended.
  2. It can issue a diagnostic.
  3. It can delete the code you have written.
  4. It can reformat the nearest disk.

(all but the 4th routinely happen in practice)

Undefined behavior is very bad, and imho, the C and C++ standards apply that specification much too liberally.

Technically, violating a Requires clause results in undefined behavior.

[res.on.required]/p1:

Violation of the preconditions specified in a function’s Requires: paragraph results in undefined behavior unless the function’s Throws: paragraph specifies throwing an exception when the precondition is violated.

As noted by MSN, allocator_type::value_type shall be the same as container::value_type as stated in Table 99 -- Allocator-aware container requirements.

allocator_type A       Requires:  allocator_type::value_type 
                                  is the same as X::value_type.

(X denotes an allocator-aware container class with a value_type of T using allocator of type A)

So a violation such as:

std::list<int, std::allocator<long> >  

is undefined behavior. So it:

  1. can compile and execute as you intended.
  2. can issue a diagnostic.
  3. can delete the code you have written.
  4. can reformat the nearest disk.

Just very recently (within weeks of me writing this), libc++ (http://libcxx.llvm.org) has started diagnosing this undefined behavior with static_assert so that you get the bad news asap.

We decided to go this direction, instead of allowing the behavior because the containers are not set up to allow conversions among closely related types. For example:

std::list<int, std::allocator<long>>  list1;
std::list<int>                        list2 = list1;  // is specified to not work

I.e. if you start treating list1 and list2 as equivalent types because the std::allocator gets rebind'd anyway, you're going to get disappointed down the road as you discover the two lists are really different types and not designed to interoperate anyway. So it is really best to get the bad news asap, instead of finding out 2 months, or 2 years later, when you try to use them as equivalent types.

Perhaps a future standard will treat list1 and list2 as equivalent types. It is mostly technically possible (std::is_same would likely not work). But there are no proposals that I've heard of in this direction. This direction does not seem likely to me. And with static_assert, the error is easily diagnosable. Instead I would like to see the standard move in the direction of making this code ill-formed instead of undefined. The hardest part in doing so would be word-smithing the standard, and not in the std::lib implementation.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • If `std::list` was a `using` alias for a real type, it could do the `rebind` prior to becoming a "real" type. But I think `using` aliases fail in some pattern match situations, so that won't work, bah. – Yakk - Adam Nevraumont Jul 31 '13 at 13:39
  • "In English: an ill-formed program shall have a diagnostic associated with it." Even an ill-formed program might not produce a diagnostic since ODR violations don't have to be diagnosed. – bames53 Jul 31 '13 at 17:51
6

EDIT: In [containers.requirements.general], the Allocator-aware container requirements indicate that the allocator_type::value_type is the same as Container::value_type.

So it's ill formed to pass in an allocator type with a different value_type, although at least one implementation simply uses allocator_traits<...>::rebind<value_type> to get the correct allocator.

MSN
  • 53,214
  • 7
  • 75
  • 105
  • 2
    N3485 has in container.requirements.general, Table 99 "Allocator-aware container requirements" says that `allocator_type::value_type` must be the same as `X::value_type`. Is this different in the true C++11? – aschepler Jul 30 '13 at 22:06
  • @aschepler, ah, good point. I missed that table. That implies that it's ill formed to use a different allocator type, even though most implementations support it. – MSN Jul 30 '13 at 22:15
  • Thank you for the answer. I did find [this SO question](http://stackoverflow.com/questions/15224988/custom-allocator-for-stdvectorchar-is-ignored), so it seems libstdc++ has been `rebind`ing always and I don't know of an implementation that doesn't do it. However, it is interesting that they are all in violation of the standard. – Jesse Good Jul 31 '13 at 03:05