5

Background

I'm writing an image handling class. For this question two requirements of the class are of interest:

  • Must have "deep" const correctness.
  • Must allow sub-image aliases, a.k.a. sections or slicing without performing deep copies of image data.

By deep constness I mean the following. Consider this example class:

class Image{
public:
    ...
    Image(const Image&);

    Image alias(int x0, int y0, int x1, int y1) const;
private:
    Buffer image_data;
};

And assume you have a function that is passed a const reference to a image, like so:

void foo(const Image& im){
    Image alias = im.alias(0,0, 10, 10);
    // Hold on, the mutable alias can modify the data in the const argument!
}

The above alias function call defeats the constness of the argument because the contents of the argument image can be modified through the non-const alias. I can't return a const Image from alias because well that makes no sense really and the copy constructor of Image will happily construct a mutable instance anyway.

My solution (and I've seen it used often so I don't claim originality here) is to introduce two classes (interfaces really, but for the simplicity they are classes in this example and return by value is okay, in reality alias(...) returns a shared_ptr to interface):

class ConstImage{
public:
    ConstImage alias(...) const;
};

class Image{
public:
    Image alias(...);
};

Now the following code:

void foo(const Image& im){ 
    // compile error, cannot convert ConstImage to Image, perfect!
    Image alias = im.alias(...); 
}

And of course you would change the signature to void foo(const ConstImage& im) so that you can accept invocations like foo(const_im.alias(...)) or other ConstImage objects.

Now of course we want foo(const ConstImage& im) to be callable (1) with a mutable Image too and here is where the question comes in.

Question

Using inheritance: class Image : public ConstImage will make all Image types usable through ConstImage references and pointers. It will also facilitate code-reuse between the const and non-const implementations which by necessity are very similar.

This solves the above mentioned problem (1). However if you look at the inheritance relation as an "is a" statement then "a mutable image IS AN immutable image" makes no sense. Which speaks against using inheritance here.

My question is, is this kind of (ab)use of inheritance widely acceptable? Is there some other better solution that I have overlooked? Note that the Image class is more of an interface so naive value conversion from Image to ConstImage isn't possible. Although one could probably come up with something crazy to allow it.

  • Another option to consider: Maybe make alias() return a simpler ImageRef class (somewhat inspired by string_ref from Boost), that only has const methods and holds a non-owning reference/pointer to the underlaying data. – glampert Oct 1 '15 at 15:10
  • @glampert unfortunately that's not possible in this case. The return type from alias() const must be a first class Image. There will also be a mutating version of alias(). – Emily L. Oct 1 '15 at 15:20
2

Is there some other better solution that I have overlooked

An Image "is a" ConstImage in the sense that it can always be "treated as const". Using inheritance strictly to model "IS A" relationships is also known as "Liskov substitution principle" (LSP), which is about behaviour. The LSP is fulfilled when an inherited class can always substitute its base class without violating the correctness of the program. And as long as you do not call a non-const method of an Image (which you typically won't do when passing it through a ConstImage reference), an Image will always behave like a ConstImage.

  • An IImage will only contain common methods Image and ConstImage, so IImage will not provide any non-const operators. – Doc Brown Oct 1 '15 at 14:40
  • And how would I pass an Image where a ConstImage is expected then? The two don't become convertible just because they share a common baseclass (however both become convertible to the base class, if you suggest using IImage where ConstImage is desired, that just becomes contrieved and essentially the same as inheriting Image from ConstImage). – Emily L. Oct 1 '15 at 14:43
  • @EmilyL.: see my edit. – Doc Brown Oct 1 '15 at 14:59
2

Create an abstract class ReadableImage. Derive both MutableImage and ImmutableImage (and also ImageSlice) from this class. Then the is-a relations hold. Don't take ImmutableImage parameters in functions unless the function actually expects the image to really be immutable, for example because it expects it to not be modified by another thread.

The annoying part of this is slicing, but that's a problem that you always face with class hierarchies representing data in C++.

  • That's almost what I proposed first, just with a better name for the common base class (+1). – Doc Brown Oct 2 '15 at 8:39
  • This "solution" cannot work, nor can any other, because "const" is not a structure preserving operation (functor in the category theory sense). In particular I have a MutableImage* and up cast to an ImmutableImage* then the holder of the latter will be surprised when the image changes. – Yttrill Oct 5 '15 at 15:57
  • 2
    @Yttrill If you have a MutableImage and you cast to an ImmutableImage, you have undefined behavior, because these classes aren't related beyond sharing an ancestor. That's why the common base is called ReadableImage, so as not to imply that it cannot change, only that that this particular handle doesn't provide the means to change it. – Sebastian Redl Oct 5 '15 at 16:02
  • I'm not convinced this will work, can you please provide an example implementation? Remember that a MutableImage has to passable where an ImmutableImage is wanted. Note I may have used the word immutable wrongly, what I really mean is const as in "will not be modified here". As @deduplicator points out in their answer. – Emily L. Oct 6 '15 at 12:36
  • 1
    "Remember that a MutableImage has to passable where an ImmutableImage is wanted" - In my design, you never want an ImmutableImage, you want a ReadableImage. – Sebastian Redl Oct 6 '15 at 15:10
2

If you accept that Const just means "won't be manipulated from here", as most of your question seems to imply, a const_image isn't immutable.

Seems like your class is a smart-pointer with extra accessors and (if non-const) manipulators.

You might benefit from basing it on std::shared_ptr.

Next, to emulate deep-const, your Image-class needs to do the following:

  1. Ensure a const instance lacks manipulators.

    That's easily done, just make sure no manipulator-method is const-qualified, and all inspector-methods are.

    int getX() const; // inspector
    int setX();       // manipulator
    
  2. Ensure a const instance does not expose any non-deep-const internal state.

    Any inspector-method only returns const-qualified references or equivalent pointers, and only for state which itself emulates deep-const, or does not have any linked state.
    Naturally deep-copies may always be returned. (Deep-copies and Shallow copies are trivially equivalent for state without internal links.)

  3. Ensure a non-const instance cannot be constructed from a const instance.

    Now this last point looks like a back-breaker, as C++ unfortuately does not allow making a constructor const.
    Fortunately, we can get around that with more code: Just declare a second class.

struct image;
struct const_image {
    // ctors, dtor, const inspectors
    // no manipulators, and only expose as const_image, other deep-const type or deep-copy
private:
    friend class image;
    // explicitly defaulted (move-)assignment-operators
    // Whatever members are neccessary to maintain the data.
    // Probably only a single shared_ptr and some indices
};

struct image : const_image {
    // delegating ctors for all but copy-construction and move-constructor
    // copy-ctor accepting a non-const image, probably defaulted
    // move-constructor, (move-)assignment-operators
    // additional non-const inspectors (where possible a thin mask over the const ones)
    // these all return Image or manipulatable internal state
    // manipulators
private:
    // if neccessary, optional additional state for supporting manipulation-operations
};
  • Yes, what I really want is const as in "will not be modified here". None of the images will really reside in a ROM or anything. Your example code pretty much is what I did I my question, isn't it? I've been through a few design iterations now and seem to have landed at a ImageBuffer class and non copyable ImageView and Image = shared_ptr<ImageView> and ConstImage = shared_ptr<const ImageView>. – Emily L. Oct 6 '15 at 12:41
1

TL; DR

  1. (Personal, unproven idea)
    • "This cptr is const, that mptr is not
  2. Almost-proven idea, on top of OpenCV's Mat_<T> design
    • Bake const-ness into the template parameter T, and
    • Make sure all holes are plugged, by making some changes to OpenCV.

This is a tough one, and one that even well-known imaging libraries such as OpenCV do not find a perfect solution. Instead most people would live with pragmatism, i.e. be satisfied with whatever they have got.

Here is my unproven idea:

  • In each Image class (and Buffer class as well), maintain two raw pointer fields:
    • One is a non-const void (or uchar) pointer.
    • One is a const void (or uchar) pointer. (Meaning that it does not allow one to modify the data being pointed to, via this pointer.)
  • In Image instances that need to "enforce const-ness", even if someone has managed to obtain a non-const-qualified reference to that Image, the non-const pointer is intentionally set to nullptr.
    • Before any pixel modifications, someone will have to validate the pointer before proceeding. Typically, if one intends to modify a whole bunch of pixels, one only needs to validate the pointer once, so with proper design this should not become a performance overhead.
    • All modifications will be done via the non-const pointer, and all read-only operations will be done via the const pointer.
    • For mutable images (or, images that allow modifications), the two pointers will always have the same value.

This is just a wild, unproven idea. Feel free to explore and critique. That said, my idea is brewed from long time use of OpenCV and low-level image processing implementations.


As a bonus, if you are already familiar with OpenCV, you can try the following two declarations:

const auto sz = cv::Size(3, 3); const uchar fillval = 0U; cv::Mat_<uchar> matMutable1B(sz, fillval); cv::Mat_<const uchar> matImmutable1B(sz, fillval);

Notice that the first one allows modifications via the usual OpenCV style, e.g. mat.at(row, col) = fillval;, mat(row, col) = fillval;, mat.ptr(row)[col] = fillval;, whereas the second one doesn't allow.

This is due to the template parameter <T> being const-qualified by the second declaration.

However, as of currently, this is not foolproof, because someone who obtain the pointer via mat.data will get a uchar*, regardless of the const-qualification of template parameter <T>.


One very important distinction about immutability / object state access control versus C++ style notational const-qualification

C++ const-qualification affects code which "come by / obtain" a reference to a certain class. Thus, it passes along const-qualification by something like a chain-of-custody of types. The Image itself cannot find out whether someone owning a reference to it and making a call to it is const-qualified or not. Instead, C++ enforces this when type-checking the caller's code, and blocks the caller's attempt. The Image class never knows.

My idea of using two pointers (const / non-const) and setting the non-const to nullptr is an attempt to solve this issue at runtime. Thus, someone who obtains an Image that is not const-qualified, but its non-const data pointer is nullptr, will face runtime consequences.


Ultimately, if this is too difficult to discuss, maybe just spend some time with OpenCV, and try both const-qualify the matrix itself as well as the template parameter, to see which technique satisfies the most of your requirements.


Another lesson learned from OpenCV is that you must implement your own reference-counting mechanism in the image class. Failure to do so makes the library user's code very brittle.

1

Looks like I'm a little late to the party, but hopefully the OP sees this answer. The best solution to your problem is not to use inheritance at all, but rather leave the Image class as is. Instead, what you need are two Alias classes: Alias, and ConstAlias. Then, change all of your functions that don't really need the Image itself, but an alias to take Alias or ConstAlias as appropriate. The alias method has a const and non-const version, which return ConstAlias and Alias types respectively.

Does this sound familiar from somewhere? Replace Image with vector, and Alias with iterator, and you've got... well, half the STL.

It's better to distinguish explicitly between Images and Aliases using the type system, the former is an owner, and the latter is not. Most functionality just requires a view (possibly mutable) of the data, it doesn't need to actually mess with the ownership at all. This makes you think harder about whether your function should take an Image or just an alias. It also allows you to avoid using shared_ptr at all and stick with a much simpler and clearer (and possibly more performant) model of ownership.

  • But then Alias must be convertible to/or inherit from ConstAlias. Because functions that accept a ConstAlias must also accept a non-const alias, and we're back at the original question. Note, that your proposed design is pretty much what I have in the OP just that what you call Image, I call Buffer, and what you call Alias, I call Image. Note that it is also required that you can take sub-images of a sub-image, I.e. Alias must support the alias() function. – Emily L. Oct 12 '15 at 7:16
  • Yes, but because the Alias is ultra light weight, you can write a trivial implicit conversion. No need to mess around with inheritance. Also, it seems like my design is different from yours, your Image owns Buffer seemingly, and Buffer is not for public consumption. My Alias is not owning, and Image is used publicly. – Nir Friedman Oct 12 '15 at 13:21
  • Okay, so if I got this right. You're supposing I do not use inheritance but instead provide implicit conversion operator and live with the code duplication for the common methods? Also, although I didn't mention it explicitly but Buffer is a smart pointer type so my Image is also "thin". – Emily L. Oct 12 '15 at 14:05
  • It's a smart pointer, but it sounds like it's a shared pointer, no? I discuss this issue as well in my post. This ownership model is much more confusing, you can pass around Images and end up with entities owning Images that alias each other, which results in action at a distance. In contrast, {Const,}Alias is always short lived. I don't think there is much real code duplication, anything that is more than a one liner can be factored out into an implementation function. So it's really just the duplication of writing out the interface. Annoying, but better than a needless inheritance hierarchy. – Nir Friedman Oct 12 '15 at 14:15
  • Yes, it's shared. However an Alias will require a shared or weak ptr too. Otherwise it cannot access the image data. At any rate we both seem to be thinking of Alias/Image as a view and this is quire similar to the N3851 array_view proposal. I might glean at that... – Emily L. Oct 12 '15 at 14:20
0

I think you'd be better to come at the problem from a slightly different perspective.

C++ in many ways works best with types that act like value types. That is, they behave like built-in types or most STL types. You could make an Image class that acts like an std::vector and is a value type with 'deep' const correctness by simply copying data whenever you copy an image or take an image slice but that would be somewhat inefficient due to all the copying. Those are the semantics you'd ideally like from your Image class however.

You should think of the sharing of image data for efficiency as purely an implementation detail of your class. It's not something the end user should need to know about from an interface point of view. One way to achieve this is by using copy-on-write to give the appearance of a value type while under the hood you are sharing data wherever possible. This used to be a common implementation tactic for std::string although it is not any more, primarily because of thread safety concerns (which you'll have to address too if you want to use your Image class in multi-threaded scenarios).

One implication of this alternative approach is that alias() is a bad name for your image slice function. It exposes an implementation detail in the name of a public API. That function should instead be called slice(). With a copy-on-write implementation you could call slice() on a const Image and get an Image. Behind the scenes the image data would be shared until you tried to write to it, at which point you would make a copy. A more sophisticated implementation might tile the image and only copy tiles that were touched by the write so changing a few pixels wouldn't take a copy of the entire image slice.

  • Yes, this was my initial idea, I wanted the Image to behave like a value type. However this breaks down due to other design requirements. Polymorphy in particular. I have a ImageAny interface which provides generic access to any type of image and Image<pixelmode> template sub classes that inherits ImageAny to provide a concrete class. The user can choose to write their algorithm using ImageAny for easy code that handles all cases or cast to Image<pixelmode> for higher performance. – Emily L. Oct 6 '15 at 12:46
  • @EmilyL. you don't need to abandon value types to get polymorphic behaviour. Type erasure techniques give you the best of both worlds. Performance would still be a concern if you have an interface at the granularity of individual pixel access however. In that case static polymorphism via templates could be the way to go. – mattnewport Oct 6 '15 at 15:01
  • When you design a concept, it should ALWAYS be abstract. Never never never put data in such classes. The whole idea is to use derivation to isolate the abstract representation (by methods) from multiple possible concrete representations (none of which is ever known by name except at the point of construction). – Yttrill Oct 6 '15 at 18:13
0

This solves the above mentioned problem (1). However if you look at the inheritance relation as an "is a" statement then "a mutable image IS AN immutable image" makes no sense. Which speaks against using inheritance here.

Well ... a mutable image IS an imutable image, with extra capabilities (for mutation) - which makes a lot of sense (just like a surgeon is a doctor with extra capabilities). The fact that the extra capabilities are a distinction between mutable and immutable makes little difference (it's still an immutable image with mutable behavior added to it).

I imagine the confusion that created the question, is the fact that in spoken language, it doesn't make sense to say "a mutable X is an immutable X" (because in rational thinking, either A or non-A, but not both).

This OO "is-a" relationship though, is not really the same "is-a" relationship that you have in spoken English.

  • An immutable T is most certainly not a mutable T, as the first promises the T cannot be modified at all, not only not through that alias. Both can be accessed through a constant reference though to promise not modifying through the used alias. – Deduplicator Oct 6 '15 at 10:01
  • The "OO" is-a relation is exactly the one that one has in natural language. For example a symmetric matrix ISA matrix. It is a subtype. Every symmetric matrix is an example of a matrix. The word "sub type" means precisely the same as "subset of the values of". – Yttrill Oct 6 '15 at 18:25
  • And you may say "but there are operations on matrices which do not preserve symmetry". Good. You got it. OO doesn't work. Stop trying to build a perpetual motion machine. It's been proven, mathematically, to be impossible. In this case it is well known that the variances on a function of higher arity alternate, OO requires the second argument to be covariant, but it isn't, so OO is known and proven beyond doubt to be incapable of representing relationships (or any N-ary concept with N greater than 1). – Yttrill Oct 6 '15 at 18:31
  • Mutability/immutability speaks about a property of the object itself, whereas readability/writeability speaks about what other functions are allowed to perform on this object. The typical approach is to say that a mutable object provides both read and write access, whereas an immutable object only provides read access. There is an additional kind of object, called a read-only proxy, which provides read-only access to an otherwise mutable object. The read-only proxy is needed to prevent other functions from using type casting to get around interface-based access control. – rwong Sep 30 '16 at 19:31
  • In general, inheritance as provided by OOP languages are insufficient for implementing access control. See my answer to another question. – rwong Sep 30 '16 at 19:32
-1

The fundamental problem is you are trying to use an unsound model of types. Despite wording in the ISO C++ Standard and wrong ideas expressed by Bjarne in the ARM originally, there is no such thing as a const type. Contrarily, constness is a property of pointers only (and references but lets leave them out for simplicity). Therefore a class representing a const and non-const type is a wrong concept.

Const in C++ itself is an unsound concept, it should have been removed. This is evident in the Standard Library when trying to model the concept generically. It only works one level deep as you have observed. It applies to the access path to an object, but does not propagate to a data structure (linked collection of objects). The simplest example is a singly linked list: a const pointer to a list node immediately yields a non-const next pointer.

In a theoretical model, const and non-const abstractions are distinguished by the absence or presence of mutator methods and are therefore distinct types without any "isA" relation, whatever that is.

My best advice: stop trying to model abstractions with OO techniques because OO is proven to be so highly restricted in applicability it is not suitable for modelling any but the simplest concepts. Learn some functional programming methods instead.

  • This isn't really an answer to the problem. A plain answer as "do something else" isn't helpful, now if you had provided a concrete example of what "something else" would be in the case I have then I would up-vote. Const is useful in getting help from the compiler to avoid inadvertent writes or modifications. It also acts as a guarantee, "My image buffer won't be modified if I call that method that takes the image by const&". – Emily L. Oct 6 '15 at 12:32
  • Of course const is useful, but it also creates problems. In balance it isn't worth it. IMHO, but of course that is expressed in hindsight. ("It was a good idea at the time") The "do something else" is simple enough. Using abbrevatiations: class II_by_delegation : public virtual II { MI *delegate; ... }. That is, II and MI classes are abstract, provide an implementation of an II with wrapper methods and a pointer to a MI, obviously not wrapping the mutators. Hope that makes sense. – Yttrill Oct 6 '15 at 18:08
  • BTW: it turns out in practice an abstract representation of images is useless although possible. It is actually better to have unrelated classes for JPG, PNG, etc and just accept your application has to have a lot of methods, some of which work on some kinds and not others. GIF and PNG for example allow a list of frames which can represent motion but JPG does not. All these kinds do admit a conversion to a bitmap but in much modern practice you're using a renderer that works with a specific set of compact representation directly. – Yttrill Oct 6 '15 at 18:40
  • Apparently we have very different views of what is useless and what is not; and what is good design and what is not. You make a lot of bold statements without reference or without sufficient motivation besides: "it is so, you must believe me". While I respect your opinion and attempt at helping me (although it looks like a rant at OO in disguise), I think you are plain wrong and will stick to my prior knowledge. – Emily L. Oct 7 '15 at 14:01
  • Sorry but it is hard in the restricted space of a comment here to present a proper case. Read some academic papers on subtyping and variance. The bottom line is that the variances permitted for a function in a sound type system alternate, so that the first argument may be covariant (accept a subtype) but the second one must be contravariant (accept only a super type). Binary methods require both arguments (the object being the first argument) to be covariant. Hence OO cannot be soundly typed. Therefore it cannot be a general paradigm. – Yttrill Oct 8 '15 at 15:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.