【问题标题】:Initializing C++ const fields after the constructor在构造函数之后初始化 C++ const 字段
【发布时间】:2011-03-28 18:37:28
【问题描述】:

我想创建一个不可变的数据结构,比如说,可以从文件初始化。

class Image {
public:
   const int width,height;
   Image(const char *filename) {
     MetaData md((readDataFromFile(filename)));
     width = md.width();   // Error! width  is const
     height = md.height(); // Error! height is const
   }
};

我可以做些什么来解决这个问题

class Image {
   MetaData md;
public:
   const int width,height;
   Image(const char *filename):
     md(readDataFromFile(filename)),
     width(md.width()),height(md.height()) {}
};

然而

  1. 它迫使我将 MetaData 作为一个字段保存在我的对象中。我并不总是想要。
  2. 有时构造函数中的逻辑比单次读取要复杂得多(例如,错误处理可能需要几行代码)

所以我想到的唯一解决方案是

class A {
  int stub;
  int init(){/* constructor logic goes here */}
  A():stub(init)/*now initialize all the const fields you wish
  after the constructor ran */{}
};

有更好的主意吗? (在Java 中,您可以在构造函数中初始化finals)。

【问题讨论】:

  • 为什么Image的成员必须是const?一个不可变的数据结构最好用一个数据结构类的const 实例来表示,而不是一个所有成员恰好都是const 的数据结构类的实例。如果您遵循这种方法,那么您的构造函数中没有任何问题;对象的const-ness 仅在构造函数完成后才开始。
  • 差不多这个 - 不要让成员变量 const,真的。
  • @Charles,我希望所有图像都尽可能 const。我不希望在我之后的任何程序员在Image 中的方法中更改对象的大小。我不想跟踪谁更改了这个图像大小,如果两个位置的图像大小不同,我可以断定这是 100% 的内存泄漏,而不是一个懒惰的程序员制作快捷方式。有很多正当理由偏爱强制和通信某个领域一定不能改变。
  • 也许改变构造函数接受宽度和高度作为参数可以解决问题。
  • @Elazar Leibovich:我真的不明白你在保护什么。如果人们要在不与您交流的情况下编辑您的课程(添加方法),那么他们要做的第一件事就是在 const 妨碍他们时立即删除它。对我来说,感觉就像你正在尝试设计更适合你如何使用你的类而不是如何使用它的限制。

标签: c++ constructor constants


【解决方案1】:

你可以抛弃构造函数中的常量:

class Image {
public:
    const int width,height;
    Image(const char *filename) : width(0), height(0) {
        MetaData md(readDataFromFile(filename));

        int* widthModifier = const_cast<int*>(&width);
        int* heightModifier = const_cast<int*>(&height);
        cout << "Initial width " << width << "\n";
        cout << "Initial height " << height << "\n";
        *widthModifier = md.GetWidth();
        *heightModifier = md.GetHeight();
        cout << "After const to the cleaners " << width << "\n";
        cout << "After const to the cleaners " << height << "\n";
    }
};

这将实现您想要做的,但我必须说我个人会远离它,因为它会根据标准导致未定义的行为(摘自cppreference

const_cast 可以形成一个引用或指针 非 const 类型实际上是指一个 const 对象... 通过非常量修改 const 对象 访问路径...导致未定义的行为。

我会担心任何公共数据成员(至少在您的特定示例中)。 我会采用 Georg 的方法,或者将数据设为私有并仅提供 getter。

【讨论】:

  • 想过,但我不太喜欢这种方法。但这可能是迄今为止最好的方法。我只需要正确记录它(ImitateJavasFinalChangableInConstructor...)。
  • @Elazar:停止尝试用 C++ 编写 Java。用 C++ 编写 C++,忘记你懂 Java。
  • @GMan,我不在乎它是否是 Java。将widthheight 设置为const 是个好主意,JavaScalaHaskellCC++。在Java 中更容易做到,在C++ 中,您必须通过箍。我真的不认为 const 字段的范式是 Java 特定的,或者取决于 Java 的特殊特征。
  • @GMan,顺便说一句,我的问题是找出 C++ 的惯用方法是在你的类中有一个依赖于计算的 const 字段。答案很可能是——不可能。
  • @Elazar:是的,有 - a) 使用函数来初始化它们或 b) 将它们包装在自定义类中,进行计算在其 ctor 中,并具有该类的 const 成员。
【解决方案2】:

与 Java 相比,这是我最不喜欢的 C++ 方面之一。当我需要解决这个问题时,我将使用我正在研究的示例。

下面是 readObject 方法的等价物。它从提供的文件路径反序列化视频密钥。

#include <fstream>
#include <sstream>
#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>

using namespace std;
using namespace boost::filesystem;
using namespace boost::archive;

class VideoKey
{
  private:
    const string source;
    const double fps;
    const double keyFPS;
    const int numFrames;
    const int width;
    const int height;
    const size_t numKeyFrames;
    //Add a private constructor that takes in all the fields
    VideoKey(const string& source,
      const double fps,
      const double keyFPS,
      const int numFrames,
      const int width,
      const int height,
      const size_t numKeyFrames)
    //Use an initializer list here
    : source(source), fps(fps), keyFPS(keyFPS), numFrames(numFrames), width(width), height(height), numKeyFrames(numKeyFrames)
    {
      //Nothing inside this constructor
    }
  public:
    //Then create a public static initializer method that takes in
    //the source from which all the fields are derived
    //It will extract all the fields and feed them to the private constructor
    //It will then return the constructed object
    //None of your fields are exposed and they are all const.
    const static VideoKey create(const path& signaturePath)
    {
      const path keyPath = getKeyPath(signaturePath);
      ifstream inputStream;
      inputStream.open(keyPath.c_str(), ios::binary | ios::in);
      if (!inputStream.is_open())
      {
        stringstream errorStream;
        errorStream << "Unable to open video key for reading: " << keyPath;
        throw exception(errorStream.str().c_str());
      }
      string source;
      double fps;
      double keyFPS;
      int numFrames;
      int width;
      int height;
      size_t numKeyFrames;
      {
        binary_iarchive inputArchive(inputStream);
        inputArchive & source;
        inputArchive & fps;
        inputArchive & keyFPS;
        inputArchive & numFrames;
        inputArchive & width;
        inputArchive & height;
        inputArchive & numKeyFrames;
      }
      inputStream.close();
      //Finally, call your private constructor and return
      return VideoKey(source, fps, keyFPS, numFrames, width, height, numKeyFrames);
    }

【讨论】:

    【解决方案3】:
    class A
    {
    public:
        int weight,height;
    
    public:
        A():weight(0),height(0)
        {
        }
    
        A(const int& weight1,const int& height1):weight(weight1),height(height1)
        {
            cout<<"Inside"<<"\n";
        }
    };
    
    static A obj_1;
    
    class Test
    {
        const int height,weight;
    
    public:
        Test(A& obj = obj_1):height(obj.height),weight(obj.weight)
        {
        }
    
        int getWeight()
        {
            return weight;
        }
    
        int getHeight()
        {
            return height;
        }
    };
    
    int main()
    {
        Test obj;
    
        cout<<obj.getWeight()<<"\n";
    
        cout<<obj.getHeight()<<"\n";
    
        A obj1(1,2);
    
        Test obj2(obj1);
    
        cout<<obj2.getWeight()<<"\n";
    
        cout<<obj2.getHeight()<<"\n";
    
        return 0;
    }
    

    据我了解,我认为这种机制会起作用。

    【讨论】:

    • Erm...heightwidth 不是 const,所以我真的不明白你的意思...
    • 如果您考虑测试类,那么您可以在那里看到 height 和 wieght 是 const 。因此,如果您认为类 Image 只不过是 Test 类,在这种情况下,您会发现那里的成员变量是 const 。实际上,我写这篇文章的目的是显示不嵌入类 obj 即类 A,您可以从类 A 为 Test 类分配 const 变量,在这种情况下,实际上复制构造函数也将充当默认构造函数。所以我认为我的目的是得到解决。
    【解决方案4】:

    如果是 C++0x,我会推荐这个(委托构造函数):

    class Image
    {
      public:
    
        const int width, height;
    
        Image(const char* filename) : Image(readDataFromFile(filename)) { }
        Image(const MetaData& md) : width(md.width()), height(md.height()) { }
    };
    

    【讨论】:

      【解决方案5】:

      您可以在这里简单地使用NamedConstructor 成语:

      class Image
      {
      public:
        static Image FromFile(char const* fileName)
        {
          MetaData md(filename);
          return Image(md.height(), md.width());
        }
      
      private:
        Image(int h, int w): mHeight(h), mWidth(w) {}
      
        int const mHeight, mWidth;
      };
      

      命名构造函数的主要优点之一是其显而易见性:名称表明您正在从文件构建对象。当然它稍微冗长一些:

      Image i = Image::FromFile("foo.png");
      

      但这从来没有困扰过我。

      【讨论】:

      • @Matthie,这在某种程度上解决了。但是,您隐式使用了 Image 的复制构造函数,这不是很好。 (我猜你一定会喜欢 C++...)
      • @Elazar:为什么不那么好?
      • 实际上,即使复制是可能的,复制构造函数也不太可能在这里执行:返回值优化负责性能。请注意,在 C++0x 中,我们将在此处使用 move 语义,尽管也没有必要调用移动构造函数。
      • @GMan,好吧,如果我想将图像 DATA 作为图像中的字段保留在堆栈中?它有时在实时嵌入式系统中是有意义的。也许我使用的资源不能让两个相同的类指向它们,在这种情况下,我希望这个类的复制构造函数是私有的。
      • @Elazar:在 C++0x 中,您将正确定义移动构造函数并将复制构造函数设为私有,它会起作用。如果没有移动语义,它会很混乱(不幸的是)。不过,我仍然不明白您的示例,显然 DATA 将是图像的私有字段。
      【解决方案6】:

      我会使用静态方法:

      class Image {
      public:
          static Image* createFromFile( const std::string& filename ) {
              //read height, width...
              return new Image( width, height ); 
          } 
      
          //ctor etc...
      }
      

      【讨论】:

      • @Matthieu M:从来不用新的吗?不要在静态成员函数中使用 new ?有用的评论?我希望你不要像这里那样评论你的代码。
      • @Sam,@Frank,谁会删除你刚刚创建的新图像?你怎么知道Image的析构函数事后没有删除呢?这种做法问题很大。
      • @Sam: "请不要使用new [unnecessarily] please..." (仅当对象的生命周期需要独立于范围时才有必要。当然,你想要自动分配的对象上的可移动或共享语义以避免泄漏。)
      • 好的,长版本:通过复制返回将更加有效(RVO),并且不会导致任何对象生命周期控制问题。短版本注定要采用 Java/C# 编程风格,在 C++ 中,每次创建对象时都不必使用new,这通常是有害的。请注意,我没有使用ever,我只是建议不要在这里使用new。至于我的代码?我不评论它;)
      • 是的,我同意在这种情况下,返回副本会更有意义。我想我对多态类型等的摆弄太多了;)
      【解决方案7】:

      如何将 MetaData 作为参数传递给构造函数。这带来了很多好处:

      a) 构造函数接口明确了对 MetaData 的依赖。 b) 它有助于使用不同类型的 MetaData(子类)测试 Image 类

      所以,我可能会建议类似于以下内容:

      struct MD{
         int f(){return 0;}
      };
      
      struct A{
         A(MD &r) : m(r.f()){}
         int const m;
      };
      
      int main(){}
      

      【讨论】:

      • 这使得初始化A 变得更加困难,有时我需要初始化许多复杂的MD 类型和逻辑。
      【解决方案8】:

      您应该为宽度和高度添加内联 getter,而不是公共 const 成员变量。编译器将使该解决方案与原始尝试一样快。

      class Image {
      public:
         Image(const char *filename){ // No change here
           MetaData md((readDataFromFile(filename)));
           width = md.width();
           height = md.height();
         }
         int GetWidth() const { return width; }
         int GetHeight() const { return height; }
      private:
         int width,height;
      };
      

      P.S.:我过去常常在最后写一些私人的东西,因为它们对班级的用户来说不太重要。

      【讨论】:

      • 我希望Image 的未来实现者不要在内部更改widthheight。请参阅我对 GMan 的评论。您在 P.S. 中的观点很好!谢谢!
      • @Elazar Leibovich:派生类将看不到宽度和高度。非派生类中的方法可能会引起麻烦。 Mathieu M. 摇滚 :-) 使用他的静态工厂方法。
      • 我不是在谈论将从我的代码中的类继承的客户端。我在谈论将更改我的代码并修复我的代码中的错误的开发人员。毕竟他是我的队友……我想和这个人清楚地交流“我们在对象的生命中从未接触过这个领域”。如果编译器会阻止他这样做,我会很高兴。添加const 字段将迫使他向审阅者解释为什么可以删除他们的constness。将它们设为私有不会告诉他他不能修补我的班级来更改它们。
      【解决方案9】:

      首先,您应该了解构造函数体只是为了运行代码以完成将您的对象作为一个整体进行初始化;在进入正文之前,成员必须完全初始化。

      Ergo,所有 成员都在一个(隐含的,除非明确表示)初始化列表中进行初始化。显然,const 变量必须在列表中初始化,因为一旦你进入正文,它们就已经被初始化了;您只是试图分配它们。

      通常,您没有const 成员。如果您希望这些成员是不可变的,请不要向它们提供任何可能更改它们的公共访问权限。 (另外,拥有const 成员会使您的类不可分配;通常是不必要的。)采用这条路线很容易解决您的问题,因为您只需按照您的意愿在构造函数的主体中为它们分配值。

      在保持const 的同时做你想做的事情的方法可能是:

      class ImageBase
      {
      public:
          const int width, height;
      
      protected:
          ImageBase(const MetaData& md) :
          width(md.width()),
          height(md.height())
          {}
      
          // not meant to be public to users of Image
          ~ImageBase(void) {} 
      };
      
      class Image : public ImageBase
      {
      public:
          Image(const char* filename) : // v temporary!
          ImageBase(MetaData(readDataFromFile(filename)))
          {}
      };
      

      我认为这条路线不值得。

      【讨论】:

      • 我希望这些字段在对象内部是不可变的。我想强制实现者永远不要改变Image 的宽度或高度。我不能单独用 getter 和 setter 来强迫它。我知道这就是 C++ 的工作方式(ctor 在成员之后执行),但 java 也可以这样工作,并且有针对 finals 的解决方法。
      • @Elazar:嗯,这不是 Java,所以没有必要提及它。我认为你最好的选择是将你的不可变因素分解到一个基类中,你的 Image 可以继承它。更好的是,因为无论如何它都是一个基础,只需摆脱 const 并再次提供 const 访问器。因为它是基类并且控制它,所以你知道没有其他东西可以操纵这些值。
      • 如果您在一个小团队中工作,这是个好主意。在一家大公司和一个会经过很多人手的代码库中,我想我必须假设下一个更改这个类的人可能不是我(或者可能是我,他已经忘记了他在任何地方都假设 Image 类是不可变的)。
      【解决方案10】:

      您可以将widthheight 移动到一种类型中,并将初始化代码移动到初始化辅助函数中:

      // header:
      struct Size { 
          int width, height;
          Size(int w, int h) : width(w), height(h) {}
      };
      
      class Image {
          const Size size; // public data members are usually discouraged
      public:
          Image(const char *filename);
      };
      
      // implementation:
      namespace {
          Size init_helper(const char* filename) {
              MetaData md((readDataFromFile(filename)));
              return Size(md.width(), md.height());
          }
      }
      
      Image::Image(const char* filename) : size(init_helper(filename)) {}
      

      【讨论】:

      • 这是我已经准备好的一个简单的解决方案。 (我没有对您投反对票,但可能是因为我提出了一个非常相似的解决方案,并且您的解决方案没有解决主要问题)。如果我有 10 个这样的字段怎么办?如果我有一些复杂的错误处理代码来初始化每个变量怎么办?这并不总是有效。
      • @Elazar:也许我错过了一些东西,但我在你的问题中没有看到同样的方法?如果需要,您还可以始终将更多字段添加到可以是嵌套类的辅助结构中。我也不明白为什么初始化助手中的错误处理不能更复杂?
      • @GeorgFritzsche 不错的解决方案,但图像可以私下继承大小,这样可以从初始化它们的基类直接访问高度/宽度
      猜你喜欢
      • 1970-01-01
      • 2010-11-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-02
      • 1970-01-01
      相关资源
      最近更新 更多