0

Without relying on const_cast, how can one make a C++ data member const after but not during construction when there is an expensive-to-compute intermediate value that is needed to calculate multiple data members?

The following minimal, complete, verifiable example further explains the question and its reason. To avoid wasting your time, I recommend that you begin by reading the example's two comments.

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

Output:

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

(I am aware of this previous, beginner's question, but it treats an elementary case. Please see my comments in the code above. My trouble is that I have an expensive initialization I do not wish to perform twice, whose intermediate result I do not wish to store; whereas I still wish the compiler to protect my constant data members once construction is complete. I realize that some C++ programmers avoid constant data members on principle but this is a matter of style. I am not asking how to avoid constant data members; I am asking how to implement them in such a case as mine without resort to const_cast and without wasting memory, execution time, or runtime battery charge.)

FOLLOW-UP

After reading the several answers and experimenting on my PC, I believe that I have taken the wrong approach and, therefore, asked the wrong question. Though C++ does afford const data members, their use tends to run contrary to normal data paradigms. What is a const data member of a variable object, after all? It isn't really constant in the usual sense, is it, for one can overwrite it by using the = operator on its parent object. It is awkward. It does not suit its intended purpose.

@Homer512's comment illustrates the trouble with my approach:

Don't overstress yourself into making members const when it is inconvenient. If anything, it can lead to inefficient code generation, e.g. by making move-construction fall back to copy constructions.

The right way to prevent inadvertent modification to data members that should not change is apparently, simply to provide no interface to change them—and if it is necessary to protect the data members from the class's own member functions, why, @Some programmer dude's answer shows how to do this.

I now doubt that it is possible to handle const data members smoothly in C++. The const is protecting the wrong thing in this case.

thb
  • 13,796
  • 3
  • 40
  • 68
  • 2
    Members are initialized in the member initializer list, before constructor body even starts. Don't even think about `const_cast`, [it is not allowed to modify `const` values](https://stackoverflow.com/questions/3593687/two-different-values-at-the-same-memory-address) even if you cast `const` away. Is there a reason to use `const` data mambers anyway? It's usually a bad idea, since it limits your class in several ways. – Yksisarvinen Nov 05 '22 at 22:28
  • 2
    Don't overstress yourself into making members const when it is inconvenient. If anything, it can lead to inefficient code generation, e.g. by making move-construction fall back to copy constructions. – Homer512 Nov 05 '22 at 22:31
  • 1
    @Homer512 There's a way around that, using a mutable bool to keep track if it's moved from (i.e., if the dtor should be skipped). – lorro Nov 05 '22 at 22:34
  • 1
    @lorro I meant it more in the way that default-generated move constructors will have to invoke copy-constructors for const attributes with move and copy constructors. E.g. having a ```const std::string``` member may be bad idea. Of course you can always work around this with custom move constructors but it's easy to forget and can turn into a performance gotcha – Homer512 Nov 05 '22 at 22:42
  • @Yksisarvinen The example's second comment gives the reason. However, I have heard advice like yours before and am aware of it. I neither dispute the advice nor concur with it, except that the advice avoids the question I have asked. – thb Nov 05 '22 at 22:44
  • @Homer512 That is a good point of which I had not thought: move construction. However, the example class manages no resources; it just keeps data on the stack. The class in my real code (which spans several files and is far too long and boring to post here) has a *member* that manages resources, but as far as I know the member's move constructor is unimpeded by the fact that the member is `const`. Am I mistaken? – thb Nov 05 '22 at 23:13
  • 1
    @thb the issue is that the argument to the constructor will be a const reference if the moved object is const. You can see it in action here: https://godbolt.org/z/1r4jY7jx8 – Homer512 Nov 05 '22 at 23:24
  • *"The following minimal, complete, verifiable example explains the question and its reason better than mere English prose can."* -- I disagree. The key piece of prose that your question is missing is an explanation that there is an expensive-to-compute intermediate value that is needed to calculate multiple data members. Mix in your post-code explanation, and add the information from the comments that you asked people to read before reading the code. It seems possible to flesh that out to a piece of prose that explains the question better than your code does. – JaMiT Nov 05 '22 at 23:57
  • @JaMiT Ok. Updated. – thb Nov 06 '22 at 00:06

4 Answers4

8

Something along these lines perhaps:

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) {}
};
Igor Tandetnik
  • 50,461
  • 4
  • 56
  • 85
  • Clever. It works. I had not known that a constructor can treat its own class as it were a base class. – thb Nov 05 '22 at 23:00
  • 3
    The search term for further research is [delegating constructor](https://en.cppreference.com/w/cpp/language/constructor#Delegating_constructor) – Igor Tandetnik Nov 05 '22 at 23:10
6

One possible way could be to put a and b in a second structure, which does the expensive calculation, and then have a constant member of this structure.

Perhaps something like this:

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

With that said, why make a_ and b_ constant in the first place, if you control the class T and its implementation?

If you want to inhibit possible modifications from other developers that might work on the T class, then add plenty of documentation and comments about the values not being allowed to be modified. Then if someone modifies the values of a_ or b_ anyway, then it's their fault for making possibly breaking changes. Good code-review practices and proper version control handling should then be used to point out and possibly blame wrongdoers.

Some programmer dude
  • 400,186
  • 35
  • 402
  • 621
  • The reason to make `a_` and `b_` constant is the same as the reason to make any other object constant: to tell the compiler not to let me make a stupid mistake later. I prefer the compiler to forbid me to do anything except exactly what I intend to do! The compiler cannot always forbid me from making a stupid mistake, of course, but `const` helps in case my unit test has left a gap. – thb Nov 05 '22 at 22:50
  • I will study it further but, on first review, I believe that your technique achieves my goal. – thb Nov 05 '22 at 22:53
1

Before describing the answer, I'd first suggest you to re-think your interface. If there's an expensive operation, why don't you let the caller be aware of it and allow them to cache the result? Usually the design forms around the calculations and abstractions that are worth keeping as a state; if it's expensive and reusable, it's definitely worth keeping.

Therefore, I'd suggest to put this to the public interface:

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)
  {}
};

Note that ExpensiveResult can be directly constructed from int n (ctor is not explicit), therefore call syntax is similar when you don't cache it; but, caller might, at any time, start storing the result of the expensive calculation.

lorro
  • 10,687
  • 23
  • 36
  • You are quite right that the expensive and the reusable are worth keeping. My actual code (which spans several files and is far too long and boring to post here) happens to have an intermediate production which—though used more than once during construction—is not worth keeping. This is why I have asked. – thb Nov 05 '22 at 23:03
1

It's pretty easy to modify the const ints in your object as a result of a significant change in c++20. The library function construct_at and destroy_at have been provided to simplify this. For your class, destroy_at is superfluous since the class contains no members that use dynamic memory like vector, etc. I've made a small modification, added a constructor taking just an int. Also defined an operator= which allows the objects to be manipulated in containers. You can also use construct_at to decrement a_ and b_ in your operator-- method. Here's the code:

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

Output:

before decrement, t() == 2
after  decrement, t() == 1
t.a() == 259
t.b() == 649
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
doug
  • 3,840
  • 1
  • 14
  • 18