如何处理常量对象中非常量引用成员的初始化?

7
假设你有一个类:
    class C 
    {
      int * i;

      public:

         C(int * v):i(v) {};

         void method() const;  //this method does not change i
         void method();        //this method changes i
    }

现在,您可能希望定义该类的常量实例。
    const int * k = whatever;
    const C c1(k); //this will fail

但是这将失败,因为非const int C的构造函数C(int * v)。
所以你需要定义一个const int构造函数。
    C(const int * v):i(v) {}; //this will fail also

但是这也会失败,因为C的成员“int * i”是非const的。

在这种情况下应该怎么办?使用mutable?强制转换?准备类的const版本?

编辑:在与Pavel(下面)的讨论后,我对这个问题进行了一些调查。对我来说,C++所做的并不正确。指针目标应该是一个严格的类型,这意味着您不能执行以下操作:

int i;
const int * ptr;
ptr = & i;

在这种情况下,语法将const视为不更改指针目标的承诺。另外int * const ptr也是一个承诺,不更改指针值本身。因此,您可以应用两个地方的const。然后您可能希望您的类模拟一个指针(为什么不呢)。在这里,事情开始变得复杂。C++语法提供了const方法,能够承诺不更改字段值本身,但却没有语法来指出您的方法不会更改您内部类指针的目标。
解决方法是定义两个类const_CC等。这并不是一个最优路线。使用模板和其部分特化很容易陷入混乱。而且所有可能的参数变化,例如const const_C & argconst C & argconst_C & argC & arg看起来都不是很美观。我真的不知道该怎么办。使用单独的类或const_casts,每种方式似乎都是错误的。
在两种情况下,我应该将不修改指针目标的方法标记为const吗?还是只需遵循传统路径,即const方法本身不会更改对象的状态(const方法不关心指针目标)。然后在我的情况下,所有方法都将是const,因为类正在模拟一个指针,因此指针本身是T * const。但明显有些方法会修改指针的目标,而其他方法则不会。

mutable 在这里不起作用,因为它只适用于字段本身,而不适用于它们所指向的内容。 - Pavel Minaev
我有一种感觉,似乎常量正确性出了问题。一定是有什么误解。 - mip
7个回答

13
听起来你想要一个对象,它可以包装int*(然后表现为非const),或int const*(然后表现为const)。你不能用一个单一的类来完全实现它。
事实上,应用于你的类的const应该改变其语义的这种想法是错误的——如果你的类模拟指针或迭代器(如果它包装指针,很可能是这种情况),那么应用于它的const应该只意味着它本身不能被更改,并且不应暗示任何关于所指向值的内容。你应该考虑遵循STL对其容器的做法——这正是它具有不同的iteratorconst_iterator类的原因,两者都是不同的,但前者可以隐式转换为后者。此外,在STL中,const iteratorconst_iterator并不相同!所以就这样做吧。 [编辑] 这里有一种巧妙的方法,可以在保证始终正确处理const的情况下,在Cconst_C之间最大程度地重用代码,而不会陷入未定义行为(使用const_cast):
template<class T, bool IsConst>
struct pointer_to_maybe_const;

template<class T>
struct pointer_to_maybe_const<T, true> { typedef const T* type; };

template<class T>
struct pointer_to_maybe_const<T, false> { typedef T* type; };

template<bool IsConst>
struct C_fields {
   typename pointer_to_maybe_const<int, IsConst>::type i;
   // repeat for all fields
};


template<class Derived>
class const_C_base {
public:
    int method() const { // non-mutating method example
        return *self().i;
    }
private:
    const Derived& self() const { return *static_cast<const Derived*>(this); }
};

template<class Derived>
class C_base : public const_C_base<Derived> {
public:
    int method() { // mutating method example
        return ++*self().i;
    }
private:
    Derived& self() { return *static_cast<Derived*>(this); }
};


class const_C : public const_C_base<const_C>, private C_fields<true> {
    friend class const_C_base<const_C>;
};

class C : public C_base<C>, private C_fields<false> {
    friend class C_base<C>;
};

如果你只有很少的字段,复制它们到两个类中可能会更容易,而不是使用结构体。如果有很多字段,但它们都是相同类型的,则直接将该类型作为类型参数传递即可,不必使用const包装器模板。


继承的真正问题不在于字段的重复 - 您可以将其重构为结构体,然后在您的const_C版本中全面应用const。问题在于代码重用。首先,从C继承const_C没有意义 - 您希望C隐式转换为const_C,而不是相反,因此如果要使用继承,则必须从const_C派生C。但是,问题在于您从const_C继承的所有字段也将是const,并且突变C方法将无法使用任何东西... - Pavel Minaev
在字段的情况下,可以使用mutable。但是许多文档都说,如果字段在逻辑上是const但由于某种原因需要更新,则应使用mutable。在这种情况下,它将被设计为可变的。 - mip
好的,我现在可以专注于这个主题了。 “那么应用于它的const只应该意味着它本身不能被更改,并且不应该暗示任何关于指向的值的内容。” 我不同意。如果const仅适用于字段而不是它们所指向的值,则会出现这种情况。但事实并非如此!这给const正确性带来了一些不一致性。如果类被强制不修改自身,则对我来说,这种语义应该递归地影响所有字段。仅仅由于按值传递的副作用,很少需要编写类的const版本。 - mip
在C++中,const是浅层设计的,并且不会通过指针和引用进行传播。例如,const auto_ptr<T>允许您修改它所指向的对象。如果您想引用一个const对象,可以单独指定它,如auto_ptr<const T>。还要注意,const递归地应用于const类/结构的所有字段(除非它们被声明为mutable)-但是当这些字段是指针时,只有指针本身是const,而不是它们指向的内容。如果您想要一个传播的const指针,自己编写它非常简单。 - Pavel Minaev
但是当这些字段是指针时,只有指针本身是const,而不是它们所指向的内容。这就是我所说的不一致性。因为同样的结构有时会以完全相同的方式行事,而另一次则会有所不同。实际上,它并不递归地应用于所有字段,因为指针可能像任何其他字段一样成为一个字段。要精确,const是深层的,但递归级别设置为1。否则,它将无法使值指针指向const。 - mip
显示剩余26条评论

4
你的示例没有问题,k是按值传递的。成员变量i被隐式地声明为常量,因为在实例为常量时,直接成员变量C不能更改。
常量性表示在初始化后不能更改成员变量,但在初始化列表中用值初始化它们当然是允许的 - 否则你怎么给它们赋值呢?
不起作用的是在不公开构造函数的情况下调用构造函数;)
更新回答更新的问题:
是的,C++有时会强制你使用一些冗长的语法,但常量性正确性是一种普遍的标准行为,你不能随意重新定义而不破坏期望。Pavels answer已经解释了一种常见的惯用法,它被用于像STL这样的成熟库中,以解决这种情况。
有时你必须接受语言的局限性,并仍然处理接口用户的期望,即使这意味着应用显然次优的解决方案。

@doc: 然后你可以执行 const_cast<int*>(p) - 但在将 const 指针提升为非 const 指针之前,你应该三思而后行。 - Georg Fritzsche
1
非常抱歉我的粗心大意。因为 Pavel Minaev 的话,我必须取消您的答案。毕竟,我认为这不是一个坏问题,而且我在网上没有找到令人满意的解释。 - mip
你知道吗,自从我回答问题以来,问题已经进行了一些编辑,你可以先自己检查一下。 - Georg Fritzsche
第一段是误导性的,因为问题中的示例确实失败了。 - Adayah

0

我也遇到了同样不幸的问题,在感叹C++中缺少const构造函数之后,我得出结论,两个模板化是最好的选择,至少在重用方面是如此。

我的情况/解决方案的一个非常简化的版本如下:

 template< typename DataPtrT >
 struct BaseImage
 {
     BaseImage( const DataPtrT & data ) : m_data( data ) {}

     DataPtrT getData() { return m_data; } // notice that if DataPtrT is const 
                                           // internally, this will return
                                           // the same const type
     DataPtrT m_data;
 };

 template< typename DataPtrT >
 struct DerivedImage : public BaseImage<DataPtrT>
 {
 };

非常不幸的是,存在类继承的丢失,但在我的情况下,可以接受制作一种类型转换运算符,以便能够在明确了解如何在底层进行转换的情况下,在const和非const类型之间进行转换。这与适当使用复制构造函数和/或重载解引用运算符相结合,可能会使您达到想要的目标。

 template< typename OutTypeT, typename inTypeT )
 image_cast< shared_ptr<OutTypeT> >( const shared_ptr<InTypeT> & inImage )
 {
     return shared_ptr<OutTypeT>( new OutTypeT( inImage->getData() ) );
 }

0

好的,这是我目前为止所做的。为了在类的const版本之后允许继承而不需要使用const_casts或额外的空间开销,我创建了一个联合体,它基本上看起来像这样:

template <typename T>
union MutatedPtr
{
protected:
    const T * const_ptr;
    T * ptr;

public:
    /**
     * Conversion constructor.
     * @param ptr pointer.
     */
    MutatedPtr(const T * ptr): const_ptr(ptr) {};

    /**
     * Conversion to T *.
     */
    operator T *() {return ptr;}

    /**
     * Conversion to const T *.
     */
    operator const T *() const {return const_ptr;}
};

当声明MutatedPtr字段时,在const方法中返回const_ptr,而在非const方法中返回普通ptr。它将方法的const-ness委托给指针目标,在我的情况下是有意义的。
有任何评论吗?
顺便说一句,您当然可以对非指针类型或甚至方法执行类似的操作,因此引入mutable关键字似乎是不必要的(?)

1
这个问题的困扰在于,如果你先以const方式访问同一个MutatedPtr,然后再以非const方式访问它(当它是常量对象的成员时会发生这种情况——在构造函数中它将是非const的,但在其他方法中它将是const的),那么它就无法可移植地工作。这个问题之所以存在,是因为在C++中,你只能从最后写入的联合成员中读取——也就是说,如果构造函数写入了ptr,那么后来从const_ptr中读取就会引发未定义行为。 - Pavel Minaev
如果联合体有两个(或更多)结构体,则有一个例外。如果这些结构体共享成员的“公共初始序列”,则一旦其中一个被分配,该公共序列可以从任一结构体中访问。为了“共同”,字段序列必须按相同顺序包含“布局兼容”类型。任何类型T都与const T兼容。因此,如果您在结构体中包装这些指针,则应完全符合规范。 - Pavel Minaev
这个方法不能通用(也不能替代所有情况下的mutable),原因是你不能在联合体中使用具有非平凡构造函数或析构函数的类型成员(例如,没有std::string)。 - Pavel Minaev
从我所了解的情况来看,您可以从联合体的任何成员中读取。K&R(我知道它是C)指出,这样做的结果是实现定义的。Open Standards C ++甚至更加简洁。我拥有的其他书籍没有更多的说明。我认为它将在99.(9)%编译器上工作,对于其余的编译器,我已经在构造函数中添加了assert(ptr == const_ptr)。我想知道C ++的匿名联合是否不受此限制。 - mip
我还发现,你可以在union中拥有带有非平凡构造函数的类型,只需提供自定义构造函数、析构函数、赋值运算符和复制构造函数就可以了。下面是来自Open Standards C++ 的示例,其中包含std::string作为union的成员: union U { int i; float f; std::string s; };由于std::string(21.3)声明了所有特殊成员函数的非平凡版本,因此U将具有隐式删除的默认构造函数、复制构造函数、复制赋值运算符和析构函数。要使用U,必须用户声明一些或所有这些成员函数。 - mip
显示剩余3条评论

0

当您实例化时

const C c1(...)

因为c1是const,它的成员i变成了:

int* const i;

正如其他人提到的那样,这被称为隐式常量。

现在,在你的例子中稍后,你尝试传递一个const int*。所以你的构造函数基本上是在做这个:

const int* whatever = ...;
int* const i = whatever; // error

你得到错误的原因是因为你不能将const转换为非const。'whatever'指针不允许更改它所指向的内容(int部分是const)。'i'指针允许更改它所指向的内容,但本身不能被更改(指针部分是const)。
你还提到想让你的类模拟一个指针。STL使用迭代器来实现这一点。一些实现使用的模型是有一个名为'const_iterator'的类,它隐藏了真正的指针,并且只提供了访问指向数据的const方法。然后还有一个'iterator'类,它继承自'const_iterator',添加了非const重载。这很好地工作 - 它是一个自定义类,允许与指针相同的constness,其中类型与指针镜像如下:
  • iterator -> T*
  • const iterator -> T* const
  • const_iterator -> const T*
  • const const_iterator -> const T* const
希望这有意义 :)

我已经阅读了gcc迭代器类的源代码,并发现这些类非常简单,以至于它们在每个方法中都会重复代码,而这并不适用于我的情况。另外请注意,迭代器并不是指针的模型,而是一堆指针的模型。在我的情况下,该类始终包含一个const指针(即T * const),因此我不需要四种const变体。 - mip

0
你的问题没有意义。你从哪里得到了所有这些“这将失败”的预测?它们中没有一个是真实的。
首先,构造函数的参数是否声明为 const 完全无关紧要。当你按值传递参数(就像在你的情况下)时,无论参数是否声明为 const ,你都可以传递一个 const 对象作为参数。
其次,从构造函数的角度来看,对象是常量的。无论你构造什么样的对象(常量或非常量),从构造函数内部来看,对象都从未是常量。所以不需要 mutable 或其他任何东西。
为什么不尝试编译你的代码(看看什么也不会出错),而不是做出奇怪、无根据的预测,说什么东西“会失败”呢?

抱歉,那是一个错误。这个类使用指针,所以它不是按值传递的。我有一个真正的这种类型的类,我只是想展示一个干净和过度简化的例子。 - mip

0
一个const int*和一个int* const是不同的。当你的类是const时,你有后者(指向可变整数的常量指针)。你传递的是前者(指向常量整数的可变指针)。这两者是不能互换的,原因很明显。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接