Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is const-casting away const-ness of references to actual const objects permitted if they are never modified through them?

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.

like image 461
Liarokapis Alexandros Avatar asked Jun 24 '20 08:06

Liarokapis Alexandros


People also ask

Can a const reference be bound to a non const object?

No. A reference is simply an alias for an existing object. const is enforced by the compiler; it simply checks that you don't attempt to modify the object through the reference r .

Can references be const?

The grammar doesn't allow you to declare a “const reference” because a reference is inherently const . Once you bind a reference to refer to an object, you cannot bind it to refer to a different object.

Can a const reference call a non const function?

Once you have a const object, it cannot be assigned to a non-const reference or use functions that are known to be capable of changing the state of the object. This is necessary to enforce the const-ness of the object, but it means you need a way to state that a function should not make changes to an object.

Is const cast Safe?

If you cast away the constness of an object that has been explicitly declared as const, and attempt to modify it, the results are undefined. However, if you cast away the constness of an object that has not been explicitly declared as const, you can modify it safely.


2 Answers

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.

like image 75
ruakh Avatar answered Sep 29 '22 10:09

ruakh


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;
}
like image 36
prog-fh Avatar answered Sep 29 '22 11:09

prog-fh