如何使数据成员在构造之后而不是在构造期间常量



在不依赖const_cast的情况下,当计算多个数据成员需要昂贵的中间值时,如何在构造之后而不是在构造期间生成C++数据成员const

下面这个最小的、完整的、可验证的例子进一步解释了这个问题及其原因。为了避免浪费时间,我建议您从阅读示例的两条注释开始。

#include <iostream>
namespace {
constexpr int initializer {3};
constexpr int ka {10};
constexpr int kb {25};
class T {
private:
int value;
const int a_;
const int b_;
public:
T(int n);
inline int operator()() const { return value; }
inline int a() const { return a_; }
inline int b() const { return b_; }
int &operator--();
};
T::T(const int n): value {n - 1}, a_ {0}, b_ {0}
{
// The integer expensive
//     + is to be computed only once and,
//     + after the T object has been constructed,
//       is not to be stored.
// These requirements must be met without reliance
// on the compiler's optimizer.
const int expensive {n*n*n - 1};
const_cast<int &>(a_) = ka*expensive;
const_cast<int &>(b_) = kb*expensive;
}
int &T::operator--()
{
--value;
// To alter a_ or b_ is forbidden.  Therefore, the compiler
// must abort compilation if the next line is uncommented.
//--a_; --b_;
return value;
}
}
int main()
{
T t(initializer);
std::cout << "before decrement, t() == " << t() << "n";
--t;
std::cout << "after  decrement, t() == " << t() << "n";
std::cout << "t.a() == " << t.a() << "n";
std::cout << "t.b() == " << t.b() << "n";
return 0;
}

输出:

before decrement, t() == 2
after  decrement, t() == 1
t.a() == 260
t.b() == 650

(我知道前面这个初学者的问题,但它涉及一个基本情况。请参阅上面代码中的注释。我的问题是,我有一个昂贵的初始化,我不想执行两次,其中间结果我不想存储;而我仍然希望编译器在构建完成后保护我的常量数据成员。我意识到一些C++程序ers原则上避免使用常量数据成员,但这是一个风格问题。我并不是在问如何避免固定的数据成员;我在问如何在我这样的情况下实现,而不使用const_cast,也不浪费内存、执行时间或运行时电池电量。(

跟进

在阅读了几个答案并在我的电脑上进行了实验后,我认为我采取了错误的方法,因此提出了错误的问题。尽管C++确实提供了const数据成员,但它们的使用往往与正常的数据范式相反。究竟什么是变量对象的const数据成员?它并不是通常意义上的常量,是吗,因为可以通过在其父对象上使用=运算符来覆盖它。这很尴尬。它不符合其预期目的。

@Homer512的评论说明了我的方法的问题:

在不方便的时候,不要过度强调让成员成为const。如果有什么不同的话,它可能会导致低效的代码生成,例如,通过使移动构造回退到复制构造。

防止无意中修改不应该更改的数据成员的正确方法显然是,简单地说,不提供更改它们的接口——如果有必要保护数据成员不受类自身成员函数的影响,为什么,@Some programmer dude的答案展示了如何做到这一点。

我现在怀疑在C++中是否有可能顺利地处理const数据成员。在这种情况下,const是在保护错误的东西。

也许有这样的东西:

class T {
private:
T(int n, int expensive)
: value{n-1}, a_{ka*expensive}, b_{kb*expensive} {}
public:
T(int n) : T(n, n*n*n - 1) {}
};

一种可能的方法是将ab放在第二个结构中,这会进行昂贵的计算,然后在这个结构中有一个常量成员。

也许是这样的:

class T {
struct constants {
int a;
int b;
constants(int n) {
const int expensive = ... something involving n...;
a = ka * expensive;
b = kb * expensive;
}
};
constants const c_;
public:
T(int n)
: c_{ n }
{
}
};

话虽如此,如果您控制类T及其实现,为什么首先要使a_b_恒定?

如果您想禁止其他可能使用T类的开发人员进行可能的修改,请添加大量关于不允许修改的值的文档和注释。那么,如果有人修改了a_b_的值,那么这是他们做出可能破坏性更改的错。然后应该使用良好的代码审查实践和适当的版本控制处理来指出并可能指责作恶者。

在描述答案之前,我首先建议您重新考虑您的界面。如果有一个昂贵的操作,为什么不让调用方知道并允许他们缓存结果呢?通常,设计是围绕着计算和抽象而形成的,这些计算和抽象值得保持为一种状态;如果它价格昂贵且可重复使用,那么它绝对值得保留。

因此,我建议将其放到公共界面:

struct ExpensiveResult
{
int expensive;
ExpensiveResult(int n)
: expensive(n*n*n - 1)
{}
};
class T
{
private:
const int a;
const int b;
T(const ExpensiveResult& e)
: a(ka * e.expensive)
, b(kb * e.expensive)
{}
};

注意,ExpensiveResult可以直接从int n构建(ctor不是explicit(,因此当您不缓存它时,调用语法是相似的;但是,调用者可能随时开始存储昂贵计算的结果。

由于c++20的重大更改,修改对象中的常量非常容易。提供了库函数CCD_ 21和CCD_。对于您的类,destroy_at是多余的,因为该类不包含像vector等使用动态内存的成员。我做了一个小修改,添加了一个只使用int的构造函数。还定义了一个operator=,它允许在容器中操作对象。您也可以在operator--方法中使用construct_at来递减a_和b_。这是代码:

#include <iostream>
#include <memory>
namespace {
constexpr int initializer{ 3 };
constexpr int ka{ 10 };
constexpr int kb{ 25 };
class T {
private:
int value;
const int a_{};
const int b_{};
public:
T(int n);
T(int n, int a, int b);
T(const T&) = default;
inline int operator()() const { return value; }
inline int a() const { return a_; }
inline int b() const { return b_; }
int& operator--();
T& operator=(const T& arg) { std::construct_at(this, arg); return *this; };
};
T::T(const int n, const int a, const int b) : value{ n - 1 }, a_{ a }, b_{ b } {}
T::T(const int n) : value{ n - 1 }
{
// The integer expensive
//     + is to be computed only once and,
//     + after the T object has been constructed,
//       is not to be stored.
// These requirements must be met without reliance
// on the compiler's optimizer.
const int expensive{ n * n * n - 1 };
std::construct_at(this, n, ka*expensive, kb*expensive);
}
int& T::operator--()
{
// implement decrements
//--a_; --b_;
const int a_1 = a_ - 1;
const int b_1 = b_ - 1;
std::construct_at(this, value, a_1, b_1);
return value;
}
}
int main()
{
T t(initializer);
std::cout << "before decrement, t() == " << t() << "n";
--t;
std::cout << "after  decrement, t() == " << t() << "n";
std::cout << "t.a() == " << t.a() << "n";
std::cout << "t.b() == " << t.b() << "n";
return 0;
}

输出:

before decrement, t() == 2
after  decrement, t() == 1
t.a() == 259
t.b() == 649

最新更新