Fundamental Types
C++ contains the same familiar keywords (e.g., int) that you recognize from C#. This is unsurprising given that both are C-like languages. There is, however, one potential landmine that can throw you into trouble. While C# explicitly defines the sizes of fundamental types (a short is a 16-bit integer, an int is a 32-bit integer, a long is a 64-bit integer, a double is a 64-bit double-precision IEEE 754 floating point number, etc.), C++ makes no such guarantees.
The smallest fundamental unit in C++ is char, which only needs to be at least large enough to hold the 96 basic characters that the C++ standard specifies, plus any other characters in the implementation’s basic character set. In theory, some implementation of C++ could define a char as 7 bits or 16 bits … almost anything is possible. But in practice you don’t need to worry too much about a char being anything other than 8 bits (the equivalent of the byte or sbyte type in C#), which is its size in Visual C++.
In C++, char, signed char, and unsigned char are three distinct types. All three are required to take up the same amount of storage in memory. So a char in practice is either signed or unsigned. Whether it is signed or unsigned is implementation defined (see the sidebar). In Visual C++ the char type is, by default, signed. But you can use a compiler switch to have it treated as unsigned instead. In GCC, whether it is signed or unsigned depends on which CPU architecture you are targeting.
The signed integer types, in size order from smallest to largest, are:
- signed char
- short int
- int
- long int
- long long int
The only guarantee of the size of each of these integer types is that each one is at least as large as the next smallest integer type. In Visual C++, an int and a long int are both 32-bit integers. It is only the long long int that is a 64-bit integer.
Note: You can simply write long or long long; you do not need to write long int or long long int, respectively. The same is also true for short int (i.e. you can just write short). The short type is a 16-bit signed integer in Visual C++.
Each of the integer types has a corresponding unsigned integer type. You just put the keyword unsigned in front to get the unsigned version (except for signed char, which you change to unsigned char).
If you need to ensure that you are using specific sizes, you can include the C++ Standard Library header file cstdint
(e.g., #include <cstdint>
), which defines, among other things, the types:
- int8_t
- int16_t
- int32_t
- int64_t
- uint8_t
- uint16_t
- uint32_t
- uint64_t
These types have their use, but you will find that most APIs do not use them; instead, they use the fundamental types directly. This can make your programming confusing, as you constantly need to check the underlying fundamental type to ensure you do not end up with unintended truncation or expansion.
These types might come into use more, so I recommend checking for their usage in major libraries and APIs from time to time and adjusting your code accordingly if they become widely adopted. Of course, if you absolutely need a variable to be, for example, an unsigned 32-bit integer, you should certainly make use of uint32_t and make any adjustments for API calls and portability as needed.
Floating-point numbers are the same as far as size order rules. They go from float to double to long double. In Visual C++, float is a 32-bit floating-point number and double and long double are both 64-bit floating point numbers (long double is not larger than double, in other words).
C++ does not have any native type that is comparable to C#’s decimal type. However, one of the nice things about C++ is there are typically a large number of free or inexpensive libraries that you can license. For example, there’s the decNumber library, the Intel Decimal Floating Point Math Library, and the GNU Multiple Precision Arithmetic Library. None are exactly compatible with C#’s decimal type, but if you are writing for Windows systems only, then you can use the DECIMAL data type to get that compatibility if needed, along with the Decimal Arithmetic Functions and the Data Type Conversion Functions.
There is also a Boolean type, bool, which can be true or false. In Visual C++, a bool takes up a byte. Unlike in C#, a bool can be transformed into an integer type. When false, it has an integer-equivalent value of 0, and when true, it has a value of 1. So the statement bool result = true == 1; will compile and result will evaluate to true when the statement has been executed.
Then there is the wchar_t type, which holds a wide character. A wide character’s size varies based on the platform. On Windows platforms, it is a 16-bit character. It is the equivalent of C#’s char type. It is frequently used to construct strings. We will discuss strings in another chapter since many variants can be used for strings.
Lastly, there is the void type, which is used the same way it is in C#. And there is a std::nullptr_t type, which is messy to explain properly, but basically is there to be the type of the nullptr literal, which is what you should use instead of NULL or a literal 0 (zero) to check for null values.
Enumerations
Enumerations are fairly similar to each other in C++ and C#. C++ has two types of enums: scoped and un-scoped.
A scoped enumeration is defined as either an enum class or an enum struct. There is no difference between the two. An un-scoped enumeration is defined as a plain enum. Let’s look at a sample:
Sample: EnumSample\EnumSample.cpp
#include <iostream> #include <ostream> #include <string> #include "../pchar.h" enum class Color { Red, Orange, Yellow, Blue, Indigo, Violet }; // You can specify any underlying integral type you want, provided it fits. enum Flavor : unsigned short int { Vanilla, Chocolate, Strawberry, Mint, }; int _pmain(int /*argc*/, _pchar* /*argv*/[]) { Flavor f = Vanilla; f = Mint; // This is legal since the Flavor enum is an un-scoped enum. Color c = Color::Orange; //c = Orange; // This is illegal since the Color enum is a scoped enum. std::wstring flavor; std::wstring color; switch (c) { case Color::Red: color = L"Red"; break; case Color::Orange: color = L"Orange"; break; case Color::Yellow: color = L"Yellow"; break; case Color::Blue: color = L"Blue"; break; case Color::Indigo: color = L"Indigo"; break; case Color::Violet: color = L"Violet"; break; default: color = L"Unknown"; break; } switch (f) { case Vanilla: flavor = L"Vanilla"; break; case Chocolate: flavor = L"Chocolate"; break; case Strawberry: flavor = L"Strawberry"; break; case Mint: flavor = L"Mint"; break; default: break; } std::wcout << L"Flavor is " << flavor.c_str() << L" (" << f << L"). Color is " << color.c_str() << L" (" << static_cast<int>(c) << L")." << std::endl << L"The size of Flavor is " << sizeof(Flavor) << L"." << std::endl << L"The size of Color is " << sizeof(Color) << L"." << std::endl; return 0; }
This code will give the following output:
Flavor is Mint (3). Color is Orange (1). The size of Flavor is 2. The size of Color is 4.
As you can see in the sample, the scoped Color enumeration requires you to access its members in the same way as C# by prefacing the enumeration member with the enumeration’s name and the scope resolution operator. By contrast, the un-scoped Flavor enumeration allows you simply to specify the members without any prefix. For this reason, I think it’s better practice to prefer scoped enumerations: You minimize the risks of naming collisions and lessen namespace pollution.
Notice that there is another difference with scoped enumerations: When we wanted to output the numerical value of the scoped Color enum, we had to use the static_cast operator to convert it to an int, while we did not need to do any casting for the un-scoped Flavor enumeration.
For the Flavor enumeration, we specified the underlying type as being an unsigned short int. You can also specify the underlying type for scoped enumerations. Specifying the underlying type is optional, but is mandatory if you wish to use forward declaration with an un-scoped enumeration. Forward declaration is a way to speed up program compile times by only telling the compiler what it needs to know about a type rather than forcing it to compile the whole header file the type is defined in.
We will look at this later on. For now, just remember that an un-scoped enumeration must have its underlying type explicitly specified in order to use a forward declaration of it; a scope enumeration does not require specification of its underlying type to use a forward declaration of it (the underlying type will be int if none is specified).
You can do the same thing with enumerations in C++ as you can in C# in terms of explicitly assigning values to members, and in terms of creating flag enumerations. You do it all the same way, except you don’t need to apply anything like the FlagAttribute in C++ to create flag enumerations; you just assign the correct values and proceed from there.
std::wcout
, std::wcerr
, std::wcin
The std::wcout << L”Flavor... code outputs wide character data to the standard output stream. In the case of a console program such as this, the standard output is the console window. There is also a std::wcerr output stream, which will output wide character data to the standard error output stream. This is also the console window, but you can redirect std::wcout output to one file and std::wcerr output to another file. There is also a std::wcin for inputting data from the console. We won’t explore this, nor will we explore their byte counterparts: std::cout, std::cerr, and std::cin.
Just to let you see how the input looks, here’s an example.
Sample: ConsoleSample\ConsoleSample.cpp
#include <iostream> #include <ostream> #include <string> #include "../pchar.h" struct Color { float ARGB[4]; void A(float value) { ARGB[0] = value; } float A(void) const { return ARGB[0]; } void R(float value) { ARGB[1] = value; } float R(void) const { return ARGB[1]; } void G(float value) { ARGB[2] = value; } float G(void) const { return ARGB[2]; } void B(float value) { ARGB[3] = value; } float B(void) const { return ARGB[3]; } }; // This is a stand-alone function, which happens to be a binary // operator for the << operator when used with a wostream on // the left and a Color instance on the right. std::wostream& operator<<(std::wostream& stream, const Color& c) { stream << L"ARGB:{ " << c.A() << L"f, " << c.R() << L"f, " << c.G() << L"f, " << c.B() << L"f }"; return stream; } int _pmain(int /*argc*/, _pchar* /*argv*/[]) { std::wcout << L"Please input an integer and then press Enter: "; int a; std::wcin >> a; std::wcout << L"You entered '" << a << L"'." << std::endl; std::wcout << std::endl << L"Please enter a noun (one word, no spaces) " << L"and then press Enter: "; std::wstring noun; // wcin breaks up input using white space, so if you include a space or // a tab, then it would just put the first word into noun and there // would still be a second word waiting in the input buffer. std::wcin >> noun; std::wcerr << L"The " << noun << L" is on fire! Oh no!" << std::endl; Color c = { { 100.0f/255.0f, 149.0f/255.0f, 237.0f/255.0f, 1.0f } }; // This uses our custom operator from above. Come back to this sample // later when we've covered operator overloading and this should make // much more sense. std::wcout << std::endl << L"Cornflower Blue is " << c << L"." << std::endl; return 0; }
The previous code is a fairly simple demo. It has no error checking, for instance. So, if you enter an incorrect value for the integer, it will just run through to the end with std::wcin returning instantly without any data (that’s what it does unless and until you resolve the error).
If you are interested in iostream programming, including using things like std::wofstream to output data to a file and std::wifstream to read data in from a file (they work the same as std::wcout and std::wcin, just with added functionality for dealing with the fact that they work with files), see the MSDN iostream programming pages. Learning all the ins and outs of streams could easily fill a book just on its own.
One last thing though. You’ve undoubtedly noticed that the stream functionality looks a bit odd with the bit shifting operators << and >>. That’s because these operators have been overloaded. While you would expect the bit shift operators to act a certain way on integers, there isn’t any specific expectation you’re likely to have about how they should work when applied to an output stream or an input stream, respectively. So the C++ Standard Library streams have co-opted these operators to use them for inputting and outputting data to streams. When we want the ability to read in or write out a custom type that we’ve created (such as the previous Color structure), we simply need to create an appropriate operator overload. We’ll learn more about operator overloading later in the book, so don’t worry if it’s a bit confusing right now
Classes and Structures
The difference between a class and a structure in C++ is simply that a structure’s members default to public whereas a class' members default to private. That's it. They are otherwise the same. There is no value-type versus reference-type distinction as there is in C#.
That said, typically you will see programmers use classes for elaborate types (combinations of data and functions) and structures for simple data-only types. Normally, this is a stylistic choice that represents the non-object-oriented origins of structure in C, making it easy to differentiate quickly between a simple data container versus a full-blown object by looking to see if it's a structure or a class. I recommend following this style.
Note: An exception to this style is where a programmer is writing code that is meant to be used in both C and C++. Since C does not have a class type, the structure type might instead be used in ways similar to how you would use a class in C++. I’m not going to cover writing C-compatible C++ in this book. To do so, you would need to be familiar with the C language and the differences between it and C++. Instead, we are focusing on writing clean, modern C++ code.
In Windows Runtime (“WinRT”) programming, a public structure can only have data members (no properties or functions). Those data members can only be made up of fundamental data types and other public structures—which, of course, have the same data-only, fundamental, and public-structures-only restrictions. Keep this in mind if you are working on any Metro-style apps for Windows 8 using C++.
You will sometimes see the friend keyword used within a class definition. It is followed by either a class name or a function declaration. What this code construct does is give that class or function access to the non-public member data and functions of the class. Typically, you’ll want to avoid this since your class should normally expose everything you want to expose through its public interface. But in those rare instances where you do not wish to publicly expose certain data members or member functions, but do want one or more classes or functions to have access to it, you can use the friend keyword to accomplish this.
As classes are a very important part of C++ programming, we will explore them in much more detail later in the book.
Unions
The union type is a bit odd, but it has its uses. You will encounter it from time to time. A union is a data structure appearing to hold many data members, but only allowing you to use one of its data members at any one time. The end-result is a data structure that gives you many possible uses without wasting memory. The size of the union is required to be large enough only to contain the largest member of the union. In practice, this means the data members overlap each other in memory (hence, you can only use one at a time). This also means you have no way of knowing what the active member of a union is unless you keep track of it somehow. There are many ways you could do that, but putting a union and an enum in a structure is a good, simple, tidy way of doing it. Here’s an example.
Sample: UnionSample\UnionSample.cpp
#include <iostream> #include <ostream> #include "../pchar.h" enum class SomeValueDataType { Int = 0, Float = 1, Double = 2 }; struct SomeData { SomeValueDataType Type; union { int iData; float fData; double dData; } Value; SomeData(void) { SomeData(0); } SomeData(int i) { Type = SomeValueDataType::Int; Value.iData = i; } SomeData(float f) { Type = SomeValueDataType::Float; Value.fData = f; } SomeData(double d) { Type = SomeValueDataType::Double; Value.dData = d; } }; int _pmain(int /*argc*/, _pchar* /*argv*/[]) { SomeData data = SomeData(2.3F); std::wcout << L"Size of SomeData::Value is " << sizeof(data.Value) << L" bytes." << std::endl; switch (data.Type) { case SomeValueDataType::Int: std::wcout << L"Int data is " << data.Value.iData << L"." << std::endl; break; case SomeValueDataType::Float: std::wcout << L"Float data is " << data.Value.fData << L"F." << std::endl; break; case SomeValueDataType::Double: std::wcout << L"Double data is " << data.Value.dData << L"." << std::endl; break; default: std::wcout << L"Data type is unknown." << std::endl; break; } return 0; }
As you can see, we define an enum that has members representing each of the types of members of the union. We then define a structure that includes both a variable of the type of that enum and then an anonymous union. This gives us all the information we need to determine which type the union is currently holding within one encapsulated package.
If you wanted the union to be usable in multiple structures, you could declare it outside of the structure and give it a name (e.g., union SomeValue { ... };). You could then use it within the structure as, for example, SomeValue Value;. It’s usually better to keep it as an anonymous union though, since you do not need to worry about the side effects of making a change except within the structures in which it is defined.
Unions can have constructors, destructors, and member functions. But since they can have only one active data member ever, it rarely makes any sense to write member functions for a union. You will rarely see them, perhaps never.
typedef
The first thing to understand about typedef is that despite the implications of its name, typedef does not create new types. It is an aliasing mechanism that can be used for many things.
It is used a lot in implementing the C++ Standard Library and other template-based code. This is, arguably, its most important use. We will explore it more in the chapter on templates.
It can save you from a lot of typing (though this argument lost some of its force with the repurposing of the auto keyword for type deduction in C++11). If you have a particularly complicated data type, creating a typedef for it means you only need to type it out once. If your complicated data type’s purpose is unclear, giving it a more semantically meaningful name with a typedef can help make your program easier to understand.
It is sometimes used as an abstraction by developers to easily change a backing type (e.g., from a std::vector to a std::list) or the type of a parameter (e.g., from an int to a long). For your own internal-use-only code, this should be frowned upon. If you are developing code that others will be using, such as a library, you should never try to use a typedef in this way. All you are doing is decreasing the discoverability of breaking changes to your API if you change a typedef. Use them to add semantic context, sure, but do not use them to change an underlying type in code that others rely on.
If you need to change the type of something, remember that any change to a function’s parameters is a breaking change as is a change in return type or the addition of a default argument. The proper way to handle the possibility of a future type change is with abstract classes or with templates (whichever is more suitable or whichever you prefer, if both will serve). This way the public interface to your code will not change, only the implementation will. The Pimpl idiom is another good way to keep a stable API while retaining the freedom to change implementation details. We will explore the Pimpl idiom, short for “pointer to implementation,” in a later chapter.
Here is a small code block illustrating the syntax for typedef.
class ExistingType; typedef ExistingType AliasForExistingType;
And the following is a brief sample showing how typedef might be used. The purpose of this sample is to illustrate a simplified but realistic use of a typedef. In practice, a typedef like this would go into a namespace and would then be included in a header file. Since we haven’t covered any of that, this example has been kept simple intentionally.
Sample: TypedefSample\TypedefSample.cpp
#include <iostream> #include <ostream> #include <vector> #include <algorithm> #include "../pchar.h" // This makes WidgetIdVector an alias for std::vector<int>, which has // more meaning than std::vector<int> would have, since now we know that // anything using this alias expects a vector of widget IDs // rather than a vector of integers. typedef std::vector<int> WidgetIdVector; bool ContainsWidgetId(WidgetIdVector idVector, int id) { return (std::end(idVector) != std::find(std::begin(idVector), std::end(idVector), id) ); } int _pmain(int /*argc*/, _pchar* /*argv*/[]) { WidgetIdVector idVector; // Add some id numbers to the vector. idVector.push_back(5); idVector.push_back(8); // Output a result letting us know if the id is in the // WidgetIdVector. std::wcout << L"Contains 8: " << (ContainsWidgetId(idVector, 8) ? L"true." : L"false.") << std::endl; return 0; }
Conclusion
You should now have a clear understanding of the types available in C++. In the next article, we'll take a closer look at namespaces in C++.
This lesson represents a chapter from C++ Succinctly, a free eBook from the team at Syncfusion.
Comments