【问题标题】:Is this too non-mutable to const?这对 const 来说太不可变了吗?
【发布时间】:2014-03-26 15:56:25
【问题描述】:

基本上,我有以下情况。注意:void* 用于表示任意数据,在实际应用中是强类型的。

class A
{
public:
   //uses intermediate buffer but doesn't change outward behavior
   //intermediate buffer is expensive to fill
   void foo(void* input_data);

   //uses intermediate buffer and DOES explicitly change something internally
   //intermediate buffer is expensive to fill
   void bar(void* input_data);

   //if input_data hasn't changed since foo, we can totally reuse what happened in foo
   //I cannot check if the data is equal quickly, so I allow the user to pass in the
   //assertion (put the honerous on them, explicitly tell them in the comments
   //that this is dangerous to do, ect)
   void bar(void* input_data, bool reuse_intermediate);
private:
   void* intermediate_buffer_;
   void* something_;
};

因此,为了 const 的正确性,intermediate_buffer_ 永远不会暴露,因此它符合使用 mutable 变量的定义。如果我从未重用过这个缓冲区,或者我在使用缓存值之前检查了相等的 input_data,那将是故事的结尾,但由于重用_中间体,我觉得我暴露了一半,所以我不确定是否或者以下是否有意义。

class A
{
public:
   //uses intermediate buffer but doesn't change something
   //intermediate buffer is expensive to fill
   void foo(void* input_data) const;

   //uses intermediate buffer and DOES explicitly change something internally
   //intermediate buffer is expensive to fill
   void bar(void* input_data);

   //if input_data hasn't changed since foo, we can totally reuse what happened in foo
   //I cannot check if the data is equal quickly, so I allow the user to pass in the
   //assertion (put the honerous on them, explicitly tell them in the comments
   //that this is dangerous to do, ect)
   void bar(void* input_data, bool reuse_intermediate);

   //an example of where this would save a bunch of computation, though
   //cases outside the class can also happen
   void foobar(void* input_data)
   {
      foo(input_data);
      bar(input_data,true);
   }
private:
   mutable void* intermediate_buffer_;
   void* something_;
};

想法?

【问题讨论】:

  • mutable 似乎是合理的。但也许只是将其重命名为bar(void* input_data, bool fast),而不是暗示存在中间体。如果您不希望用户伤害自己,或者将其设为 private 而不是 public
  • 使用T 而不是void 会更清楚。 void * 具有特殊语义。

标签: c++ constants mutable


【解决方案1】:

我认为这是对mutable 的不当使用。根据我的经验,mutable 用于辅助私有成员变量,其本质上不能声明为 const,但不要修改公共接口的“概念常量”。

以 Mutex 成员变量和“线程安全的 getter”为例:

class Getter { 
public: 

    Getter( int d, Mutex & m ) : guard_( m ), data_( d ) { };

    int get( ) const { Lock l(guard_); return data_; };

private:

    mutable Mutex guard_;
    const int data_;
};

这里的要点是声明的数据mutable(在这种情况下是守卫)确实发生了变化(它被锁定和解锁)但是从用户的角度来看这对常量没有影响。 最终,尽管 Mutex 是可变的,但您仍然无法更改 const data_ 成员变量并且编译器会强制执行此操作

在您的情况下,您确实希望intermediate_buffer 为const,但您通过声明它是可变的来明确告诉编译器它不是。结果是您可以更改数据而编译器对此无能为力

看到区别了吗?

如果您真的希望接口符合 const 协议,请通过以下方式使其明确:

    class A { 
    public:    

        A( void* input_data );// I assume this deep copies.

        void foo() const;

        void bar();

        private:    
            const void* intermediate_buffer_;   
            void* something_; 
    };

现在责任真正由用户承担,并由编译器强制执行,而不管 cmets 说什么,也无需使用 mutable。如果他们知道 input_data 已更改,他们将不得不创建一个新的,最好是 const。

【讨论】:

【解决方案2】:

与其将中间对象隐藏在 const 对象中,不如将其公开并让foobar 返回一个副本。您正在非常明确地表明正在共享中间对象,并提供了一种新的功能来保持手头上的多个对象。如果你想隐藏实现细节,你可以暴露一个空类,并使中间对象成为这个基类的子类。

class A
{
public:
   class Intermediate
   {
      //empty
   };

   //uses intermediate buffer but doesn't change outward behavior
   //intermediate buffer is expensive to fill
   //if cache_ptr is NULL the intermediate buffer is discarded
   void foo(void* input_data, Intermediate** cache_ptr = NULL) const;

   //uses intermediate buffer and DOES explicitly change something internally
   //intermediate buffer is expensive to fill
   void bar(void* input_data, Intermediate** cache_ptr = NULL);
private:
   class IntermediateImpl : public Intermediate
   {
      //...
   };
   void* something_;
};

【讨论】:

    【解决方案3】:

    直接回答您的问题。 如果函数 foo 是 const,在任何时候调用它都不应该改变下一个操作的结果。

    例如:

    A a(some_big_data);
    a.bar(some_big_data, true);
    

    应该给出完全相同的结果(不包括性能差异)

    A a(some_big_data);
    a.foo(some_different_big_data);
    a.bar(some_big_data, true);
    

    由于 foo 是 const,用户会期望结果相同。 如果是这种情况,那么制作缓冲区mutable 是合理的。 否则,很可能是错误的。

    希望对你有帮助

    【讨论】:

      【解决方案4】:

      免责声明:我的回答并不提倡使用void*,我希望这只是为了演示目的,您实际上不需要自己使用它。

      如果重复使用相同的输入数据时可以节省大量计算,则将input_data设为成员变量。

          class A
          {
            public:
              A(void * input_data)
              {
                  set_input_data(input_data);
              }
      
              //Note, expensive operation
              void set_input_data(void* input_data)
              {
                  this->input_data = input_data;
                  fill_intermediate_buffer();
              }
      
              void foo() const;
              void bar() const;
            private:
              void * input_data;
              void * intermediate_buffer;
              void * something;
          };
      

      这显然只是一个大纲,没有更多关于input_dataintermediate_buffersomething 是什么或它们如何被使用或共享的详细信息将会丢失很多细节。我肯定会从实现中删除指针并使用std::vector。对于input_data 尤其如此,您可能希望在其中存储传入缓冲区的副本。考虑:

          A a(input);
          //modifiy input
          a.foo();
      

      使用不匹配的input_data/intermediate_buffer 对可能会得到错误的结果。

      还请注意,如果您实际上不需要 input_data 用于 foobar,那么您可以从类中删除 void* input_data,但仍保留引用它的构造函数和设置器。

      【讨论】:

      • (void* 表示数据块,讨论实际应该有什么超出范围)这似乎很危险,原因有很多。 “input_data”没有明确的所有权,因此 input_data 必须数据必须保留在范围内或被复制。基本上,如果数据足够便宜以复制输入缓冲区,那么数据将足够便宜以验证没有变化。虽然我同意,但这符合const 的字母。
      • @MadScienceDreams 您可以使用shared_ptr 而不是复制 input_data。除此之外,很难想象范围在这里是一个问题,除非你说你想优化以使用相同的输入数据调用多个函数,但 input_data 不会是相同的实际对象?
      【解决方案5】:

      我想说您对mutable 的使用确实有一定的意义——但它是不正确的,而且可能很危险。如果你创建一个函数const,它就必须是这样。

      想象一下,如果您在多线程程序中使用您的类,多个线程在该类的同一个实例上运行 - 例如:

      thread1:
      
      while(1) {
          (...) //do some work
          sharedA->foo(thread1Data);
          sharedA->bar(thread1Data, true);
          (...) //do some more work
          sharedA->bar(thread1Data, true);
          (...) //do even more work
      }
      
      thread2:
      
      while(1) {
          (...) //do some different work, maybe wait for an event
          sharedA->foo(thread2Data);
          (...) //sleep for a bit
      }
      

      由于 thread2 正在调用一个 const 函数,它不应该对从 thread1 调用 bar 的输出产生任何影响 - 但如果时机正确(或错误!)它会 - 并且由于这些原因我会说在这种情况下使用mutable 来生成foo const 是错误的,即使您只使用单个线程中的类。

      【讨论】:

      • 通过这个论点,你应该永远不要使用可变的,或者互斥锁任何使用可变的函数调用。
      • 好吧,我想说的是,例如mutable 互斥锁在大多数情况下不会改变运行函数的实际结果(除非你在计算中使用函数中花费的时间或类似的,在这种情况下,在任何情况下都可能不应该将互斥锁设置为mutable) - 但正如您所做的那样更改输入数据。
      • constmutable 与线程安全无关。一些标准库类在其const API 中是线程安全的这一事实并不要求对用户定义的类型执行相同的操作。尤其是那些不打算在线程之间共享的。
      • 我一直在寻找这个,显然这取决于你是在关注 C++98 还是 C++11。根据我从 channel9.msdn.com/posts/… 收集到的信息,在 C++11 中 const 现在实际上意味着线程安全(如按位 const 或内部同步)。在 C++98 中,我会说它仍有待讨论,具体取决于您遵循的编码标准。我个人不会让示例中的 const 函数通过代码审查。
      • @Ben Voigt - 你当然是完全正确的。然而,社区中当前的“共识”似乎是 const 限定的函数应该是线程安全的。当然,这并不意味着他们必须这样做。但这确实意味着如果他们不这样做,你就违反了最小意外原则。 (当然,如果有充分的理由期望一个类根本不是线程安全的。例如,绑定到单个线程的类,就像许多 GUI 工具包使用的那样。)
      【解决方案6】:

      鉴于您无法检查 input_data 的等价性,您或许可以对其进行加密哈希并将其用于比较。这将消除对reuse_intermediate 标志的需要。

      【讨论】:

        【解决方案7】:

        mutable 用于覆盖变量或数据成员的 const-ness。 示例:

         Class A
          {
            public:
                int m;
          }
        
          void foo(const A& a);
        

        在上面的例子中,函数 foo 将被允许修改 a.m,即使你传递了一个 const 引用。 在您的示例中,我认为您不需要使用 mutable - 它可能导致非常糟糕的设计 - 因为您将写入该缓冲区。

        【讨论】:

          猜你喜欢
          • 2012-03-14
          • 2011-11-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-06-16
          • 1970-01-01
          • 2019-07-29
          相关资源
          最近更新 更多