如果从未通过它们进行修改,是否允许对实际 const 对象的引用的 const 性?

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

我有一个抽象 class 声明了 const 和非常量成员函数。为了便于讨论,假设它看起来像这样:

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

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

这用作记录的高级表示,在保存到光盘和通过有线传输时具有不同的表示。所以大多数实现只需要将它们的成员转换为所需的高级表示。

作为有效实现的示例,让我们定义 stored_record。这用于以有损格式存储高级记录:

struct stored_record
{
    int16_t foo;
};

stored_record 可以实现 record_interface 是有道理的,但由于各种原因它不能(例如,它需要 trivially_copyable)。我们可以制作一个为其实现接口的包装器:

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_;
};

现在的问题是我们不能在给定 const stored_record & 时使用包装器,因为 包装器存储一个可变引用。我们也不能让它存储非常量引用,因为它无法实现非常量 setter 函数。

现在我想知道提供一个 const_cast 消失的工厂函数是否有效 a const stored_record &const 还有 returns a const wrapper 以便实际上不能修改引用:

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 不会真正限制 returned 值是 const,一个解决方案可以是 return 一个 const_wrapper<record_wrapper> 或类似的东西。

这是 const_cast 的有效用法还是由于 const_cast 消除了对实际 const 对象的引用的 const-ness 的未定义行为 - 即使它永远不会通过它修改。

根据 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.

因此,const_cast 本身是允许的(并且定义明确),即使通过生成的非常量引用实际修改对象是未定义的行为。

因为另一个答案非常清楚 const-casting 的有效性 在你的情况下,一个(子)问题仍然存在:如何制作你的包装器 const 当您希望它实际表现为 const 时? (您的编辑)

我建议提供两个不同的接口,即两个不同的包装器, 防止 non-const 在被认为时访问包装记录 关于 const.
这个解决方案的缺点是,为了避免代码重复, 您必须明确地使可变包装器依赖于常量包装器 (然后复制调用,而不是实际代码)。

这是一个基于您的示例的简单示例:

/**
  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;
}