When to use const class member variables?

Pages: 12
So while doing something, I made a member of my class const, and i got errors, upon some cursory research I found i need a copy constructor, and to define my own = operator since the default is deleted. But my question is how common is it to use const member variables? My code below is something i was working on when i did this. I made mWageStep const because i wanted every employee to have a 0.35 cent raise step each raise and thats a number than can normally be constant since we never want it to change. I deleted all that stuff in my code so it would compile, but ill post it anyways so you can see it.

How would you solve that issue normally? is a copy constructor necessary? what about defining a custom = operator? Im unsure what exactly a good approach would be.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <iomanip>


class Employee
{
    public:
        Employee(const std::string& name, int age, float wage, bool isSalaried) :
            mName(name), mAge(age), mWage(wage), mIsSalaried(isSalaried)
        {}

        std::string GetName() const
        {
            return mName;
        }

        int GetAge() const
        {
            return mAge;
        }

        float GetWage() const
        {
            return mWage;
        }

        bool GetIsSalaried() const
        {
            return mIsSalaried;
        }

        float GetTotalHoursWorked() const
        {
            return mTotalHoursWorked;
        }

        void GiveRaise(float stepAmount)
        {
            if (stepAmount > 0.0f)
            {
                mWage += stepAmount;
            }
            else
            {
                mWage += mWageStep;
            }
        }

        void SetTotalHoursWorked(float totalHoursWorked)
        {
            mTotalHoursWorked += totalHoursWorked;
        }

    private:
        std::string mName{};
        int mAge{};
        float mWage{};
        float mWageStep{ 0.35f };
        bool mIsSalaried{};
        float mTotalHoursWorked{ 0.0f };
};

void DisplayEmployeeInformation(std::vector<Employee>& employees);
void SortByName(std::vector<Employee>& employees);
void GiveRaise(std::vector<Employee>& employee, const std::string& employeeToGiveRaise);



int main()
{
    Employee robert("Robert", 29, 15.25f, false);
    Employee sarah("Sarah", 25, 13.65f, false);
    Employee mike("Mike", 43, 3215.0f, true);
    Employee angela("Angela", 31, 19.80f, false);

    std::vector<Employee> employees = { robert, sarah, mike, angela };

    std::cout << std::fixed << std::setprecision(2);

    std::cout << "Before Sort\n\n";

    DisplayEmployeeInformation(employees);

    GiveRaise(employees, angela.GetName());

    SortByName(employees);

    std::cout << "\n\nAfter Sort\n\n";

    DisplayEmployeeInformation(employees);
}



void DisplayEmployeeInformation(std::vector<Employee>& employees)
{
    for (const auto& employee : employees)
    {
        std::cout << employee.GetName() << '\n';
        std::cout << "    -Wage: $" << employee.GetWage() << '\n';
        std::cout << "    -Age: " << employee.GetAge() << '\n';
        std::cout << std::boolalpha << "    -Is Salaried: " << employee.GetIsSalaried() << '\n';
    }
}

void SortByName(std::vector<Employee>& employees)
{
    auto sortName = [](Employee& nameA, Employee& nameB)
        {
            return nameA.GetName() < nameB.GetName();
        };

    std::sort(employees.begin(), employees.end(), sortName);
}


void GiveRaise(std::vector<Employee>& employee, const std::string& employeeToGiveRaise)
{
    for (auto& i : employee)
    {
        if (i.GetName() == employeeToGiveRaise)
        {
            i.GiveRaise(0);
        }
    }
}
The issues you describe is exactly why I think non-static member variables should not be const.

I made mWageStep const because i wanted every employee to have a 0.35 cent raise step each raise and thats a number than can normally be constant since we never want it to change.

In that case you can just make it static and const.
Last edited on
IMO, you should make every (member) variable const, unless there is a good reason why it needs to be mutable!

Use non-static const member variables for values that are initialized once per object, typically in the constructor, and that are supposed to remain unaltered for the lifetime of each object. And use static const member variables for "real" constants that are the supposed be the the same fixed value for all instances (objects) of a class. Also, if a member variable is const, then it is "safe" to be exposed as public, whereas non-const member variable should be private (or protected) and only be access in a "controlled" way, e.g. via getter/setter methods.
Last edited on
Awesome thank you both for your replies. I also learned that non integral types need to be declared as inline to be able to be initialized in the class definition in C++ 17 and later, if not you have to declare them in a separate header file, which, im glad they changed that, cause it seems annoying to have to declare a header file just for a variable.
With properly crafted modules you don't need to worry about using inline qualifiers. Entities in a module don't need to be exported so they are "local" to the module file, just export what you want to be exposed to other translation units.
Last edited on
Don't you still need to use inline even in modules if you want to define static member variables inside the class body?
Last edited on
I haven't yet created static class members inside of a class body in a module interface or partition file, so I can't speak as an expert on that particular.

I do know if you define a class method outside of the class definition in a header file inline was required. With modules you change inline with export.

Well, I did a quickie module test with a static class member. This is done using Visual Studio 2022.

test.cppm (module interface file):
1
2
3
4
5
6
7
8
9
export module test;

export class test
{
public:
   static int t;
};

int test::t = 5;

Source.cpp (same default setting as the interface file in VS):
1
2
3
4
5
6
7
8
#include <iostream>

import test;

int main()
{
    std::cout << "The answer is: " << test::t << '\n';
}

The answer is: 5

Each file is default set to compile as: Compile as C++ Module Code (/interface ) in VS.

So I'd guess the answer to your question about needing to use inline in a module interface file when defining a static class member outside the class body is "no."

Same goes for defining a class method outside the class body declaration.

Basically replace inline with export and you're "golden".

With more complex code there are other things you can do with modules that are nigh um-possible to do with non-module code.

Not that I am an expert at working with modules, I've merely read two C++20/C++23 books and walked through the example and exercises code in the books.
And if you want that static class member to also be const:
1
2
3
4
5
6
7
export module test;

export class test
{
public:
   static const int t { 5 };
};
George P wrote:
So I'd guess the answer to your question about needing to use inline in a module interface file when defining a static class member outside the class body is "no."

My question was about defining it inside the class body.

1
2
3
4
5
6
7
export module test;

export class test
{
public:
   static int t = 5;
};

Normally, if you don't use inline it will see it as merely a declaration, not a definition, so you need to define it outside the class body (like you did). I suspect the same is true for modules but I'm not sure. Modules did change some other things related to inline, i.e. member functions that are defined inside the class body are no longer implicitly marked as inline inside modules, so it wouldn't be too surprising if they had changed something about static member variables too.
Last edited on
BTW: I think having a member variable (or database attribute) for "age" is a bad idea, because a person's age changes over time, and updating the "age" whenever it is necessary for each object will be pain! Better have a member variable "date-of-birth" and then change your GetAge() method to actually compute the current age, based on "date-of-birth" value and based on the current date/time. This also comes with the bonus that the "date-of-birth" member variable can be const, whereas the "age" variable obviously needs to be mutable.
Last edited on
Consider (not withstanding kigar64551 comment re age):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <iomanip>

class Employee {
public:
	Employee(const std::string& name, int age, float wage, bool isSalaried) :
		mName(name), mAge(age), mWage(wage), mIsSalaried(isSalaried) {}

	std::string GetName() const {
		return mName;
	}

	int GetAge() const {
		return mAge;
	}

	float GetWage() const {
		return mWage;
	}

	bool GetIsSalaried() const {
		return mIsSalaried;
	}

	float GetTotalHoursWorked() const {
		return mTotalHoursWorked;
	}

	void GiveRaise(float stepAmount) {
		mWage += stepAmount > 0.0f ? stepAmount : mWageStep;
	}

	void SetTotalHoursWorked(float totalHoursWorked) {
		mTotalHoursWorked += totalHoursWorked;
	}

private:
	std::string mName;
	int mAge {};
	float mWage {};
	bool mIsSalaried;
	float mTotalHoursWorked { };

	inline const static float mWageStep { 0.35f };
};

void DisplayEmployeeInformation(const std::vector<Employee*>& employees);
void SortByName(std::vector<Employee*>& employees);
void GiveRaise(std::vector<Employee*>& employee, const std::string& employeeToGiveRaise);

int main() {
	Employee robert("Robert", 29, 15.25f, false);
	Employee sarah("Sarah", 25, 13.65f, false);
	Employee mike("Mike", 43, 3215.0f, true);
	Employee angela("Angela", 31, 19.80f, false);

	std::vector<Employee*> employees { &robert, &sarah, &mike, &angela };

	std::cout << std::fixed << std::setprecision(2);
	std::cout << "Before Sort\n\n";
	DisplayEmployeeInformation(employees);

	GiveRaise(employees, angela.GetName());
	SortByName(employees);

	std::cout << "\n\nAfter Sort\n\n";
	DisplayEmployeeInformation(employees);
}

void DisplayEmployeeInformation(const std::vector<Employee*>& employees) {
	for (const auto& employee : employees) {
		std::cout << employee->GetName() << '\n';
		std::cout << "    -Wage: $" << employee->GetWage() << '\n';
		std::cout << "    -Age: " << employee->GetAge() << '\n';
		std::cout << std::boolalpha << "    -Is Salaried: " << employee->GetIsSalaried() << '\n';
	}
}

void SortByName(std::vector<Employee*>& employees) {
	const auto sortName { [](const Employee* nameA, const Employee* nameB) {
		return nameA->GetName() < nameB->GetName();
	} };

	std::sort(employees.begin(), employees.end(), sortName);
}

void GiveRaise(std::vector<Employee*>& employee, const std::string& employeeToGiveRaise) {
	for (const auto& e : employee)
		if (e->GetName() == employeeToGiveRaise) {
			e->GiveRaise(0);
			break;
		}
}


Why use float instead of double?

kigar64551 wrote:
I think having a member variable (or database attribute) for "age" is a bad idea

What gets taught in many beginning C++ books and on a lot online tutorial sites is IMO just plain crap. Without having any explanation of why it is crap. That does a great disservice to people who want to learn how to program for the modern age. This "good enough" approach is one of the reasons why C++ gets the unfair bad rap of being insecure.

If the books and websites followed up and explained better ways to write code, great.

Not having a hard-coded Age data member would be a good example of OOP programming with Setters/Getters. Calculating age "on the fly" is a more robust method of getting an object's "age."

</rant>

seeplus wrote:
Why use float instead of double?

People still think a float is more efficient than a double and saves memory. That philosophy is still being taught as "Gospel" from the 1980's. As if modern computers don't have gigabytes of memory and have super-speedy CPUs compared to the "old days."

</m'ok, now I'm done rantin'>
I agree that double is a good default choice. It is the default type for floating-point literals in both C and C++ for a reason. One needs to realize that float has very limited precision. It's roughly 7 digits counting from the most significant digit. So if your values are in the thousands you only have about 3 digits of precision left for the decimal part. When evaluating if this is enough you don't only need to consider the values that gets stored in the variables but you also need to think about the intermediate result of each subexpression.

1
2
3
4
5
6
7
8
9
float f = 800000.0f;
f += 0.17253f;
f -= 800000.0f;
std::cout << f << "\n"; // prints 0.1875

double d = 800000.0;
d += 0.17253;
d -= 800000.0;
std::cout << d << "\n"; // prints 0.17253 


That doesn't mean float is useless and should never be used.

If the code is vectorized, either manually or automatically by the compiler, it might be able to handle twice as many values at the same time if you use float compared to double.
https://en.wikipedia.org/wiki/Single_instruction,_multiple_data

Less memory usage is nice. Just throwing more and more hardware at problems is wasteful (and potentially costly). Larger memory usage can lead to more cache misses and copying the data is likely to be slower (especially if it has to be transferred, e.g. from CPU to GPU).
https://en.wikipedia.org/wiki/Locality_of_reference

C++23 even added some optionally supported floating-point type names of which two are smaller than a normal float (16 instead of 32 bits). This is obviously for quite specialized usage but it shows that smaller floating-point types are still relevant.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1467r9.html#motivation
https://en.cppreference.com/w/cpp/header/stdfloat
https://godbolt.org/z/3cjP5xo91


None of this is likely to matter unless you use a lot of floating-point values. Using double is the "safer" choice and is much less likely to cause problems due to limited precision but, as always when dealing with floating-point numbers, your code still needs to be able to handle rounding errors (double doesn't make the errors go away, it just makes them smaller).
Last edited on
^^^ The same issues (except FP rounding / error management) are true for integers, too. A container of 64 bit integers holding values in the thousands ... is not uncommon to see. It causes the same issues... bloated binary files when dumped to disk, page faults, performance hits, etc. We have well over 50 names for some 8 or 10 integers for a reason ... all the different sizes are relevant and useful at different times, as are signed/unsigned (though there is a school of thought that unsigned is very bad to have, its still very useful at times). Its ok to say int i when you just need ONE of them even if it works on values from 0 to 100. Its fine and factoring every piece of code down to the least needed size for every item is silly. But the moment you start making a container full of something, you need to think about how much space each item in the container takes up, as that is where you get bitten -- the larger the container, the worse the bite. Sure, my PC has 64 gigs of ram -- its bigger than my hard drive just a couple of computers back!! All that means is that there are 100 times more pages in my ram for the CPU to juggle. And yes my cpu can do 1000X more operations per second that it could just 10 or so years ago, thanks to moving from 2 to 4 to 20 cores in that timeframe. All that means is that the bandwidth to feed all those caches is that much more cramped.

So .. yea... it should at least be mentioned in a computer science education that wasting space is still bad. It may help write code easier, faster, etc but there is a price to pay. That price may be near zero (eg, the int i vs char i I mentioned) or steep. There are a lot of 3d data in extreme high res where processing doubles or tenbytes etc instead of floats can stretch a 5 min process to a 20 min process ... this kind of data is common and popular.
So ive been learning this stuff more, Im just experimenting here with scenarios where i may encounter a need to use a const variable because im sure i will run into that problem at some point, and in this example i made, it does seem to be a good choice because while mWageStep in my last example needed to be static because it applied to ALL Employees, Employee ID is unique to each employee, but once set we never want it to change, so its a constant we want tied to each object, not to the class itself. Am I correctly doing this here?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

class Employee
{
public:
    Employee(const std::string& name, double wage, int employeeID)
        : mName(name), mWage(wage), mEmployeeID(employeeID)
    {}

    Employee& operator=(const Employee& other)
    {
        if (this != &other)
        {
            mName = other.mName;
            mWage = other.mWage;
            // mEmployeeID is const and already set at construction, no need to set it here
        }
        return *this;
    }

    std::string GetName() const
    {
        return mName;
    }

    double GetWage() const
    {
        return mWage;
    }

    int GetEmployeeID() const
    {
        return mEmployeeID;
    }

private:
    std::string mName{ "" };
    double mWage{ 0.0 };
    const int mEmployeeID{ 0 };
};

int main()
{
    Employee todd("Todd", 17.65, 1135491);
    Employee jake("Jake", 19.35, 2483115);
    Employee sarah("Sarah", 1535.00, 1023398);

    std::vector<Employee> employeeList{ todd, jake, sarah };

    auto lambda = [](const Employee& a, const Employee& b)
        {
            return a.GetName() < b.GetName();
        };

    std::sort(employeeList.begin(), employeeList.end(), lambda);

    for (const auto& employee : employeeList)
    {
        std::cout << employee.GetName() << " - $" << employee.GetWage() << '\n';
    }
}
Last edited on
Also, i wrote this in my notes, is it accurate?

The presence of a const member variable in a class prevents the compiler from generating a default assignment operator (operator=). This is because the assignment operator, which typically assigns all member variables, cannot modify const members once they're initialized. Therefore, when a class has const member variables and requires object assignment, a custom assignment operator is necessary. This custom operator should handle the assignment of non-const members as needed.

As for the copy constructor, the default one provided by the compiler does handle const member variables correctly. It initializes new objects by copying each member, including const members, from an existing object. The need for a custom copy constructor arises in situations where deep copying is required, such as when managing dynamically allocated resources, rather than just to handle const members.
Last edited on
Ask yourself:
Does it make sense for a to be different from b after doing a = b; ?
Does it make sense for a type to support copy construction but not copy assignment?

Another option that might make more sense for some types is to simply disable all copy operations (copy construction and copy assignment) completely.

You might also want to read what the C++ Core Guidelines has to say about this subject:
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c12-dont-make-data-members-const-or-references-in-a-copyable-or-movable-type
Last edited on
I honestly do not know. I just started learning this stuff a few days ago haha. As for that link, so basically i should never make member variables in a class const? is that what its saying? what about static const?
Ch1156 wrote:
As for that link, so basically i should never make member variables in a class const?

That was the simple advice that I gave in my first post. The link only says you shouldn't do it for classes that are copyable.

You can use const for non-copyable classes if you want but in that case you probably also want to disable the copy constructor by defining it as deleted ( = delete ).

https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c81-use-delete-when-you-want-to-disable-default-behavior-without-wanting-an-alternative
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c22-make-default-operations-consistent

Note that making a class non-copyable is an intentional decision. The default is that classes are copyable so I wouldn't make them non-copyable unless I had a reason.

One example where making a class non-copyable makes sense is to avoid slicing with "polymorphic classes" (i.e. classes that have virtual functions).
https://en.wikipedia.org/wiki/Object_slicing
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c67-a-polymorphic-class-should-suppress-public-copymove

Ch1156 wrote:
what about static const?

Static member variables are technically very similar to global variables. Use const if it makes sense.
Last edited on
Hmm, ok that makes more sense. Since the variables are in a class, maybe they don't even need to be const since they can't even be accessed without a mutator function anyways, right? I can see it being useful in cases of polymorphism like you stated, but otherwise what I'm getting is that it should be avoided unless there's no choice right?
Last edited on
Pages: 12