13

2 Questions:

  1. Is the following code well formed with defined behaviour?

  2. Is there any possible c++ implementation in which it could assert?

Code (c++11 and higher):

#include <cassert>
#include <utility>
#include <ciso646>

template<class T> 
auto to_address(T* p) { return reinterpret_cast<unsigned char const*>(p); }

/// Test whether part is a sub-object of object
template<class Object, class Part>
bool is_within_object(Object& object, Part& part)
{
    auto first = to_address(std::addressof(object)),
                 last = first + sizeof(Object);

    auto p = to_address(std::addressof(part));

    return (first <= p) and (p < last);
}

struct X
{
    int a = 0;

    int& get_a() { return a; }
    int& get_b() { return b; }
private:

    int b = 0;
};

int main()
{
    X x;

    assert(is_within_object(x, x.get_a()));
    assert(is_within_object(x, x.get_b()));
}

Note that a and b have different access specifiers.

Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • 4
    "Note that a and b have different visibility." No. They have different **access specifiers**. Both are "visible" for any sensible notion of visibility. Making things private doesn't make them disappear. – Pete Becker Dec 02 '17 at 18:42
  • What objects are we talking about? Do they fulfill any concept, like `StandardLayout`, `POD`, `TriviallyCopyable`? The more specific you get there, the more acurate your answer will be. Otherwise, the answer may only revolve around your example code, which may be likely not to be applicable to real world problems. – Jodocus Dec 02 '17 at 18:56
  • 1
    Here's where it gets interesting: `X y; assert(!is_within_object(y, x.get_a()));` This triggers exp.rel 3.3: "Otherwise, neither pointer compares greater than the other" – Todd Fleming Dec 02 '17 at 20:13
  • It is legal, and even good! It's about the same as looking at both sides before crossing the street. – Michaël Roy Dec 03 '17 at 13:22
  • What happens when X's author decides to place X::b on the heap? It's perfectty OK to change X's implementation details. But it would break your detection code. – Michaël Roy Dec 03 '17 at 13:26
  • 1
    @MichaëlRoy in my case that would be fine. I would want to detect that it was a separately allocated sub object. My use case is serialisation caching. – Richard Hodges Dec 03 '17 at 19:52
  • @PeteBecker updated in response. Thank you. – Richard Hodges Dec 12 '17 at 19:00

1 Answers1

11

Pointer comparison is defined in [expr.rel]/3-4:

Comparing unequal pointers to objects is defined as follows:

  • If two pointers point to different elements of the same array, or to subobjects thereof, the pointer to the element with the higher subscript compares greater.
  • If two pointers point to different non-static data members of the same object, or to subobjects of such members, recursively, the pointer to the later declared member compares greater provided the two members have the same access control and provided their class is not a union.
  • Otherwise, neither pointer compares greater than the other.

If two operands p and q compare equal, p<=q and p>=q both yield true and pq both yield false. Otherwise, if a pointer p compares greater than a pointer q, p>=q, p>q, q<=p, and q=p, and q>p all yield false. Otherwise, the result of each of the operators is unspecified.

What conclusions can we draw from this?

There is a total order of pointers of the same type within an object, but there is no order of pointers to different objects or of different subobjects with different access control. This lack of a general total order of pointers makes is_within_object() not very meaningful. In the cases where you'd expect it to return true, it works. In the cases where you'd expect it to return false, the result of these operators is unspecified? That's not a very useful result.


That said, we do have a giant loophole for this in the form of [comparisons]:

For templates less, greater, less_­equal, and greater_­equal, the specializations for any pointer type yield a strict total order that is consistent among those specializations and is also consistent with the partial order imposed by the built-in operators <, >, <=, >=.

So the following would be well-defined:

template<class T> 
auto byte_address(T& p) {
    return reinterpret_cast<std::byte const*>(std::addressof(p));
}

template<class Object, class Part>
bool is_within_object(Object& object, Part& part)
{
    auto first = byte_address(object);
    auto last = first + sizeof(Object);   
    auto p = byte_address(part);


    return std::less_equal<std::byte*>{}(first, p) &&
        std::less<std::byte*>{}(p, last);
}
Barry
  • 286,269
  • 29
  • 621
  • 977
  • In [comparisons]: "When a < b is well-defined for pointers a and b of type P, this implies...", so isn't it the case that these templates are only well-defined when the corresponding pointer comparisons are well-defined as well? – geza Dec 02 '17 at 21:53
  • @geza No, it doesn't. – Barry Dec 02 '17 at 22:00
  • 1
    Does this mean that these templates are actually used in a way, that builtins cannot? I mean, for example, some algorithm/container uses them in such a way? It has to be a reason that these defined this way. – geza Dec 02 '17 at 22:05
  • Why can you assert `first <= p` if `part` is really a subobject of `object`? They point neither to different elements of the same array, nor to different non-static data members of the same object. – xskxzr Dec 03 '17 at 04:38
  • It says it is a _strict total order that is consistent among those specializations_, but what makes it certain a subobject should lie within the bounds of the object? To make a contrived example, perhaps the implementation decides to set `Part` addresses' highest bit to `1`, and `Object` addresses' highest bit to `0`. This will yield a strict total order that makes the function broken. – Passer By Dec 03 '17 at 08:21
  • I'm quite confused... Isn't the result of pointer comparison in this case (using e.g. `std::less_equal`) involve unspecified behaviour? – W.F. Dec 03 '17 at 11:47
  • 1
    The `reinterpret_cast` to `unsigned char*` doesn't affect the pointer value, so the comparison is still valid. – T.C. Dec 03 '17 at 20:05
  • 2
    It's not a loophole. The historical reason why the result of comparing pointers from different allocations is unspecfied is that in a long-begone time and a far-away land (ok, I made that up), there were memory segments and "near" and "far" pointers. Comparing the latter was more expensive, but if you could assume that the latter reffered to the same memory segment (as in a single alloc.), it would be about the same cost as for a near pointer. Since `less` is used in associative containers etc., however, one needs the ability to compare arbitrary pointers, even if it is more expensive. – Arne Vogel Dec 04 '17 at 05:54