CS100 Lecture 16
Class Basics II
Contents
- Type alias members
- staticmembers
- friend
- Definition and declaration
- Destructors revisited
Type alias members
Type aliases in C++: using.
A better way of declaring type aliases:
// C-style
typedef long long LL;
// C++-style
using LL = long long;
It is more readable when dealing with compound types:
// C-style
typedef int intarray_t[1000];
// C++-style
using intarray_t = int[1000];
// C-style
typedef int (&ref_to_array)[1000];
// C++-style
using ref_to_array = int (&)[1000];
using can also declare alias templates (in later lectures), while typedef cannot.
[Best practice] In C++, Use using to declare type aliases.
Type alias members
A class can have type alias members.
class Dynarray {
 public:
  using size_type = std::size_t;
  size_type size() const { return m_length; }
};
Usage: ClassName::TypeAliasName
for (Dynarray::size_type i = 0; i != a.size(); ++i)
  // ...
Note: Here we use ClassName:: instead of object., because such members belong to the class, not one single object.
Type alias members
The class also has control over the accessibility of type alias members.
class A {
  using type = int;
};
A::type x = 42; // Error: Accessing private member of `A`.
The class has control over the accessibility of anything that is called a member of it.
Type alias members in the standard library
All standard library containers (and std::string) define the type alias member size_type as the return type of .size():
std::string::size_type i = s.size();
std::vector<int>::size_type j = v.size(); // Not `std::vector::size_type`!
                                          // The template argument `<int>`
                                          // is necessary here.
std::list<int>::size_type k = l.size();
Why?
Type alias members in the standard library
All standard library containers (and std::string) define the type alias member size_type as the return type of .size():
std::string::size_type i = s.size();
std::vector<int>::size_type j = v.size();
std::list<int>::size_type k = l.size();
- This type is container-dependent: Different containers may choose different types suitable for representing sizes.
- The Qt containers often use intassize_type.
- Define Container::size_typeto achieve good consistency and generality.
static members
static data members
A static data member:
class A {
  static int something;
  // other members ...
};
Just consider it as a global variable, except that
- its name is in the class scope: A::something, and that
- the accessibility may be restricted. Here something is private.
static data members
A static data member:
class A {
  static int something;
  // other members ...
};
There is only one A::something: it does not belong to any object of A. It belongs to the class A.
- Like type alias members, we use ClassName::instead ofobject.to access them.
static data members
A static data member:
class A {
  static int something;
  // other members ...
};
It can also be accessed by a.something (where a is an object of type A), but a.something and b.something refer to the same variable.
- If fis a function that returns an object of typeA,f().somethingalways accesses the same variable no matter whatf()returns.
- In the very first externally available C++ compiler (Cfront 1.0, 1985), fin the expressionf().somethingis not even called! This bug has been fixed soon.
static data members: Example
Suppose we want to assign a unique id to each object of our class.
int cnt = 0;
class Dynarray {
  int *m_storage;
  std::size_t m_length;
  int m_id;
public:
  Dynarray(std::size_t n)
      : m_storage(new int[n]{}), m_length(n), m_id(cnt++) {}
  Dynarray() : m_storage(nullptr), m_length(0), m_id(cnt++) {}
  // ...
};
We use a global variable cnt as the "counter". Is this a good design?
static data members: Example
The name cnt is confusing: A "counter" of what?
int X_cnt = 0, Y_cnt = 0, Z_cnt = 0;
struct X {
  int m_id;
  X() : m_id(X_cnt++) {}
};
struct Y {
  int m_id;
  Y() : m_id(Y_cnt++) {}
};
struct Z {
  int m_id;
  Z() : m_id(Z_cnt++) {}
};
static data members: Example
Restrict the name of this counter in the scope of the corresponding class, by declaring it as a static data member.
- This is exactly the idea behind staticdata members: A "global variable" restricted in class scope.
class Dynarray {
  static int s_cnt; // !!!
  int *m_storage;
  std::size_t m_length;
  int m_id;
public:
  Dynarray(/* ... */) : /* ... */, m_id(s_cnt++) {}
};
- sstands for- static.
static data members
class Dynarray {
  static int s_cnt; // !!!
  int *m_storage;
  std::size_t m_length;
  int m_id;
public:
  Dynarray(/* ... */) : /* ... */, m_id(s_cnt++) {}
};
You also need to give it a definition outside the class, according to some rules.
int Dynarray::s_cnt; // Zero-initialize, because it is `static`.
Or initialize it with some value explicitly:
int Dynarray::s_cnt = 42;
static data members
Exercise: std::string has a find member function:
std::string s = something();
auto pos = s.find('a');
if (pos == std::string::npos) { // This means that `'a'` is not found.
  // ...
} else {
  std::cout << s[pos] << '\n'; // If executed, it should print `a`.
}
std::string::npos is returned when the required character is not found.
Define npos and find for your Dynarray class, whose behavior should be similar to those of std::string.
static member functions
A static member function:
class A {
 public:
  static void fun(int x, int y);
};
Just consider it as a normal non-member function, except that
- its name is in the class scope: A::fun(x, y), and that
- the accessibility may be restricted. Here fun is public.
static member functions
A static member function:
class A {
 public:
  static void fun(int x, int y);
};
A::fun does not belong to any object of A. It belongs to the class A.
- There is no this pointer inside fun.
It can also be called by a.fun(x, y) (where a is an object of type A), but here a will not be bound to a this pointer, and fun has no way of accessing any non-static member of a.
friend
friend functions
Recall the Student class:
class Student {
  std::string m_name;
  std::string m_id;
  int m_entranceYear;
public:
  Student(const std::string &name, const std::string &id)
      : m_name(name), m_id(id), m_entranceYear(std::stol(id.substr(0, 4))) {}
  auto graduated(int year) const { return year - m_entranceYear >= 4; }
  // ...
};
Suppose we want to write a function to display the information of a Student.
friend functions
void print(const Student &stu) {
  std::cout << "Name: " << stu.m_name << ", id: " << stu.m_id
            << "entrance year: " << stu.m_entranceYear << '\n';
}
This won't compile, because m_name, m_id and m_entranceYear are private members of Student.
- One workaround is to define printas a member ofStudent.
- However, there do exist some functions that cannot be defined as a member.
friend functions
Add a friend declaration, so that print can access the private members of Student.
class Student {
  friend void print(const Student &); // The parameter name is not used in this
                                      // declaration, so it is omitted.
  std::string m_name;
  std::string m_id;
  int m_entranceYear;
public:
  Student(const std::string &name, const std::string &id)
      : m_name(name), m_id(id), m_entranceYear(std::stol(id.substr(0, 4))) {}
  auto graduated(int year) const { return year - m_entranceYear >= 4; }
  // ...
};
friend functions
Add a friend declaration.
class Student {
  friend void print(const Student &);
  // ...
};
A friend is not a member! You can put this friend delcaration anywhere in the class body. The access modifiers have no effect on it.
- We often declare all the friends of a class in the beginning or at the end of class definition.
friend classes
A class can also declare another class as its friend.
class X {
  friend class Y;
  // ...
};
In this way, any code from the class Y can access the private members of X.
Definition and declaration
Definition and declaration
For a function:
// Only a declaration: The function body is not present.
void foo(int, const std::string &);
// A definition: The function body is present.
void foo(int x, const std::string &s) {
  // ...
}
Class definition
For a class, a definition consists of the declarations of all its members.
class Widget {
public:
  Widget();
  Widget(int, int);
  void set_handle(int);
  // `const` is also a part of the function type, which should be present
  // in its declaration.
  const std::vector<int> &get_gadgets() const;
  // ...
private:
  int m_handle;
  int m_length;
  std::vector<int> m_gadgets;  
};
Define a member function outside the class body
A member function can be declared in the class body, and then defined outside.
class Widget {
public:
  const std::vector<int> &get_gadgets() const; // A declaration only.
  // ...
}; // Now the definition of `Widget` is complete.
// Define the function here. The function name is `Widget::get_gadgets`.
const std::vector<int> &Widget::get_gadgets() const {
  return m_gadgets; // Just like how you do it inside the class body.
                    // The implicit `this` pointer is still there.
}
The :: operator
class Widget {
public:
  using gadgets_list = std::vector<int>;
  static int special_member;
  const gadgets_list &get_gadgets() const;
  // ...
};
const Widget::gadgets_list &Widget::get_gadgets() const {
  return m_gadgets;
}
- The members Widget::gadgets_listandWidget::special_memberare accessed throughClassName::.
- The name of the member function get_gadgetsisWidget::get_gadgets.
Class declaration and incomplete type
To declare a class without providing a definition:
class A;
struct B;
If we only see the declaration of a class, we have no knowledge about its members, how many bytes it takes, how it can be initialized, ... - Such class type is an incomplete type. - We cannot create an object of such type, nor can we access any of its members. - The only thing we can do is to declare a pointer or a reference to it.
Class declaration and incomplete type
If we only see the declaration of a class, we have no knowledge about its members, how many bytes it takes, how it can be initialized, ... - Such class type is an incomplete type. - We cannot create an object of such type, nor can we access any of its members. - The only thing we can do is to declare a pointer or a reference to it.
class Student; // We only have this declaration.
void print(const Student &stu) { // OK. Declaring a reference to it is OK.
  std::cout << stu.getName(); // Error. We don't know anything about its members.
}
class Student {
public:
  const std::string &getName() const { /* ... */ }
  // ...
};
Destructors revisited
Destructors revisited
A destructor (dtor) is a member function that is called automatically when an object of that class type is "dead".
- For global and staticobjects, on termination of the program.
- For local objects, when control reaches the end of its scope.
- For objects created by new/new[], when their address is passed todelete/delete[].
The destructor is often responsible for doing some cleanup: Release the resources it owns, do some logging, cut off its connection with some external objects, ...
Destructors
class Student {
  std::string m_name;
  std::string m_id;
  int m_entranceYear;
public:
  Student(const std::string &, const std::string &);
  const std::string &getName() const;
  bool graduated(int) const;
  void setName(const std::string &);
  void print() const;
};
Does our Student class have a destructor?
Destructors
Does our Student class have a destructor?
- It must have. Whenever you create an object of type Student, its destructor needs to be invoked somewhere in this program. \({}^{\textcolor{red}{1}}\)
What does Student::~Student need to do? Does Student own any resources?
Destructors
Does our Student class have a destructor?
- It must have. Whenever you create an object of type Student, its destructor needs to be invoked somewhere in this program. \({}^{\textcolor{red}{1}}\)
What does Student::~Student need to do? Does Student own any resources?
- It seems that a Studenthas no resources, so nothing special needs to be done.
- However, it has two std::stringmembers! Their destructors must be called, otherwise the memory is leaked!
Destructors
To define the destructor of Student: Just write an empty function body, and everything is done.
class Student {
  std::string m_name;
  std::string m_id;
  int m_entranceYear;
public:
  ~Student() {}
};
Destructors
class Student {
  std::string m_name;
  std::string m_id;
  int m_entranceYear;
public:
  ~Student() {}
};
- When the function body is executed, the object is not yet "dead".
- You can still access its members.
    cpp ~Student() { std::cout << m_name << '\n'; }
- After the function body is executed, all its data members are destroyed automatically, in reverse order in which they are declared.
- For members of class type, their destructors are invoked automatically.
Constructors vs destructors
Student(const std::string &name)
    : m_name(name) /* ... */ {
  // ...
}
~Student() {
  // ...
}
Compiler-generated destructors
For most cases, a class needs a destructor.
Therefore, the compiler always generates one \({}^{\textcolor{red}{2}}\) if there is no user-declared destructor.
- The compiler-generated destructor is publicby default.
- The compiler-generated destructor is as if it were defined with an empty function body {}.
- It does nothing but to destroy the data members.
We can explicitly require one by writing = default;, just as for other copy control members.
Summary
Type alias members
- Type alias members belong to the class, not individual objects, so they are accessed via ClassName::AliasName.
- The class can controls the accessibility of type alias members.
static members
- static data members are like global variables, but in the class's scope.
- static member functions are like normal non-member functions, but in the class's scope. There is no this pointer in a static member function.
- A static member belongs to the class, instead of any individual object.
Summary
friend
- A friend declaration allows a function or class to access private (and protected) members of another class.
- A friend is not a member.
Definitions and declarations - A class definition includes declarations of all its members. - A member function can be declared in the class body and then defined outside. - A class type is an incomplete type if only its declaration (without a definition) is present.
Summary
Destructors - Destructors are called automatically when an object's lifetime ends. They often do some clean up. - The members are destroyed after the function body is executed. They are destroyed in reverse order in which they are declared. - The compiler generates a destructor (in most cases) if none is provided. It just destroys all its members.
Notes
\({}^{\textcolor{red}{1}}\) Objects created by new/new[] are not required to destroyed. A delete/delete[] expression will destroy it, but it is not mandatory. So you can still create an object with a deleted destructor (see \(\textcolor{red}{3}\)) by a new expression, but you can't delete it, which possibly leads to memory leak.
\({}^{\textcolor{red}{2}}\) A class can have many prospective destructors since C++20.
\({}^{\textcolor{red}{3}}\) If no user-declared destructor is provided for a class type, the compiler will always declare a destructor as an inline public member of its class.
If an implicitly-declared destructor is not deleted, it is implicitly-defined by the compiler when it is odr-used. In some very special cases the compiler may fail to define the destructor (e.g. due to a member whose destructor is inaccessible). In that case, the destructor is implicitly deleted.