10

I have an abstract class that declares const and non-const member functions. For the sake of discussion let's say it looks like this:

class record_interface
{
public:
   virtual ~record_interface() = default;

   virtual void set_foo(BoundedFloat) = 0;
   virtual BoundedFloat get_foo() const = 0;
};

This is used as a high-level representation of a record that has different representations when saved to disc and transferred via the wire. So most implementations just need to convert their members to the required high-level representation.

As an example of a valid implementation let's define stored_record. This is used to store the high-level record in a lossy format:

struct stored_record
{
    int16_t foo;
};

It makes sense that stored_record can implement record_interface but for various reasons it can't (eg. it needs to be trivially_copyable). We can make a wrapper that implements the interface for it:

class record_wrapper : public record_interface
{
public:
  record_wrapper(stored_record & wrapped)
    : wrapped_(wrapped) {}

  void set_foo(BoundedFloat value) final { wrapped_.foo = convert_to_int16(value); }
  BoundedFloat get_foo() const final { return convert_from_int16(wrapped_.foo); }

private:
  stored_record & wrapped_;
};

Now the problem is that we can't use the wrapper when given a const stored_record & since the wrapper stores a mutable reference. We also can't make it store a non-const reference as it won't be able to implement the non-const setter function.

Now I was wondering if it would be valid to provide a factory function that const_casts away a const stored_record & 's const but also returns a const wrapper so that the reference cannot actually be modified:

record_wrapper make_wrapper(stored_record & wrapped) {return {wrapped}; }
record_wrapper const make_wrapper(stored_record const & wrapped) { return {const_cast<stored_record &>(wrapped)}; }

EDIT: returning a const record_wrapper will not really restrict the returned value to be const, a solution can be to return a const_wrapper<record_wrapper> or something similar.

Is this a valid usage of const_cast or is it undefined behaviour due to const_casting away the const-ness of a reference to an actually const object - even though it is never modified through it.

  • 1
    returning a non-reference const object from a function is beyond useless and certainly doesn't do what you expect – Mestkon Jun 24 '20 at 08:48
  • Aside: `record_wrapper const make_wrapper` isn't particularly safe, as the `const` can easily disappear via copying – Caleth Jun 24 '20 at 08:48
  • @Mestkon thanks for that, see the edit. – Liarokapis Alexandros Jun 24 '20 at 09:21
  • 1
    There are [several](https://stackoverflow.com/questions/58187832/is-it-ub-to-call-a-non-const-method-on-const-instance-when-the-method-does-not-m) [questions](https://stackoverflow.com/questions/25406818/is-the-following-use-of-const-cast-undefined-behavior) of this kind, w/o accepted answer. This question will widen the collection. – Language Lawyer Jun 24 '20 at 13:58

2 Answers2

15

Per https://en.cppreference.com/w/cpp/language/const_cast:

const_cast makes it possible to form a reference or pointer to non-const type that is actually referring to a const object or a reference or pointer to non-volatile type that is actually referring to a volatile object. Modifying a const object through a non-const access path and referring to a volatile object through a non-volatile glvalue results in undefined behavior.

So, the const_cast itself is allowed (and well-defined), even though it would be undefined behavior to actually modify the object via the resulting non-const reference.

ruakh
  • 175,680
  • 26
  • 273
  • 307
  • 1
    This has a practical use; take a toy wrapper `struct Foo { int& readx(); int const& readx() const; }` With this rule, you can safely write the const version as `int const& Foo::readx() const { return const_cast(this)->readx(); }` so long as you know the `readx` non-const doesn't modify `Foo`, it just returns a mutable reference. Without this rule, UB would happen if you called it on `const Foo f`. – Yakk - Adam Nevraumont Jun 24 '20 at 18:57
2

As the other answer is perfecly clear about the validity of const-casting in your situation, one (sub-)question remains: how make your wrapper const when you want it to actually behave as const? (your edit)

I suggest providing two distinct interfaces, thus two distinct wrappers, to prevent non-const accesses to the wrapped record when it is thought about as const.
The drawback of this solution is that, in order to avoid code duplication, you have to explicitely make the mutable wrapper rely on the const wrapper (then duplicate the call, not the actual code).

Here is a simple example based on yours:

/**
  g++ -std=c++17 -o prog_cpp prog_cpp.cpp \
      -pedantic -Wall -Wextra -Wconversion -Wno-sign-conversion \
      -g -O0 -UNDEBUG -fsanitize=address,undefined
**/

#include <iostream>
#include <cstdint>

struct BoundedFloat
{
  float f;
};

struct stored_record
{
  std::int16_t foo;
};

BoundedFloat
convert_from_int16(std::int16_t v)
{
  return {float(v/100.0)};
}

std::int16_t
convert_to_int16(BoundedFloat bf)
{
  return {std::int16_t(bf.f*100.0)};
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

class const_record_interface
{
public:
  virtual ~const_record_interface() = default;
  virtual BoundedFloat get_foo() const = 0;
};

class mutable_record_interface : public const_record_interface
{
public:
  virtual void set_foo(BoundedFloat) = 0;
};

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

class const_record_wrapper : public const_record_interface
{
public:
  const_record_wrapper(const stored_record &wrapped) : wrapped_{wrapped} {}
  BoundedFloat get_foo() const final { return convert_from_int16(wrapped_.foo); }
private:
  const stored_record &wrapped_;
};

const_record_wrapper
make_wrapper(const stored_record &wrapped)
{
  return {wrapped};
}

class mutable_record_wrapper : public mutable_record_interface
{
public:
  mutable_record_wrapper(stored_record &wrapped) : wrapped_{wrapped} {}
  auto as_const() const { return make_wrapper(this->wrapped_); }
  void set_foo(BoundedFloat value) final { wrapped_.foo=convert_to_int16(value); }
  BoundedFloat get_foo() const final { return as_const().get_foo(); }
private:
  stored_record &wrapped_;
};

mutable_record_wrapper
make_wrapper(stored_record &wrapped)
{
  return {wrapped};
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

int
main()
{
  auto sr=stored_record{50};
  const auto &csr=sr;
  auto w1=make_wrapper(sr);
  auto w2=make_wrapper(csr);
  std::cout << "w1: " << w1.get_foo().f
            << "  w2: " << w2.get_foo().f << '\n';
  w1.set_foo({0.6f});
  // w2.set_foo({0.7f}); // rejected: no member named ‘set_foo'
  std::cout << "w1: " << w1.get_foo().f
            << "  w2: " << w2.get_foo().f << '\n';
  return 0;
}
prog-fh
  • 13,492
  • 1
  • 15
  • 30
  • You clearly put a lot of effort to address the underlying issue which I really appreciate. I will get back to you later. – Liarokapis Alexandros Jun 24 '20 at 11:56
  • I made a seperate question about the general design here https://softwareengineering.stackexchange.com/questions/411932/encoding-const-ness-on-interfaces-readers-writers-vs-const-wrapper – Liarokapis Alexandros Jun 24 '20 at 20:24
  • By the way I really like this solution, it's a much more classic and tried approach. My problem with this is that interfaces should guide implementation and one would usually not make this specific split without sufficient advantages. We would also not have the original problem if we could directly implement the interface on the store-record class. Consumers interested in just reading would normally accept a const reference, similarly those interested in both would chose a non-const one. So I would prefer a less intrusive solution than this if possible although it speaks close to my heart. – Liarokapis Alexandros Jun 24 '20 at 20:31