Using classes

The Resource Acquisition Is Initialization technique is useful for managing resources provided by other libraries, such as the C Runtime Library or the Windows SDK. It simplifies your code because you do not have to think about where a resource handle will go out of scope and provide clean-up code at every point. If the clean-up code is complicated, it is typical in C code to see it put at the end of a function and every exit point in the function will have a goto jump to that code. This results in messy code. In this example, we will wrap the C files functions with a class, so that the lifetime of the file handle is maintained automatically.

The C runtime _findfirst and _findnext functions allow you to search for a file or directory that matches a pattern (including wildcard symbols). The _findfirst function returns an intptr_t, which is relevant to just that search and this is passed to the _findnext function to get subsequent values. This intptr_t is an opaque pointer to resources that the C Runtime maintains for the search, and so when you are finished with the search you must call _findclose to clean up any resources associated with it. To prevent memory leaks, it is important to call _findclose.

Under the Beginning_C++ folder, create a folder called Chapter_06. In Visual C++, create a new C++ source file, save it to the Chapter_06 folder, and call it search.cpp. The application will use the Standard Library console and strings, and it will use the C Runtime file functions, so add these lines to the top of the file:

    #include <iostream> 
#include <string>
#include <io.h>
using namespace std;

The application will be called with a file search pattern and it will use the C functions to search for files, so you will need a main function that has parameters. Add the following to the bottom of the file:

    void usage() 
{
cout << "usage: search pattern" << endl;
cout << "pattern is the file or folder to search for "
<< "with or without wildcards * and ?" << endl;
}

int main(int argc, char* argv[])
{
if (argc < 2)
{
usage();
return 1;
}
}

The first thing is to create a wrapper class for the search handle that will manage this resource. Above the usage function, add a class called search_handle:

    class search_handle 
{
intptr_t handle;
public:
search_handle() : handle(-1) {}
search_handle(intptr_t p) : handle(p) {}
void operator=(intptr_t p) { handle = p; }
void close()
{ if (handle != -1) _findclose(handle); handle = 0; }
~search_handle() { close(); }
};

This class has a separate function to release the handle. This is so that a user of this class can release the wrapper resource as soon as possible. If the object is used in code that could throw an exception, the close method won't be called directly, but the destructor will be called instead. The wrapper object can be created with a intptr_t value. If this value is -1, then the handle is invalid, so the close method will only call _findclose if the handle does not have this value.

We want objects of this class to have exclusive ownership of the handle, so delete the copy constructor and copy assignment by putting the following in the public part of the class:

    void operator=(intptr_t p) { handle = p; } 
search_handle(search_handle& h) = delete;
void operator=(search_handle& h) = delete;

If an object is moved, then any handle in the existing object must be released, so add the following after the lines you just added:

    search_handle(search_handle&& h)  { close(); handle = h.handle; } 
void operator=(search_handle&& h) { close(); handle = h.handle; }

The wrapper class will be allocated by a call to _findfirst and will be passed to a call to _findnext, so the wrapper class needs two operators: one to convert to an intptr_t, so objects of this class can be used wherever an intptr_t is needed, and the other so that object can be used when a bool is needed. Add these to the public part of the class:

    operator bool() const { return (handle != -1); } 
operator intptr_t() const { return handle; }

The conversion to bool allows you to write code like this:

    search_handle handle = /* initialize it */; 
if (!handle) { /* handle is invalid */ }

If you have a conversion operator that returns a pointer, then the compiler will call this in preference to the conversion to bool.

You should be able to compile this code (remember to use the /EHsc switch) to confirm that there are no typos.

Next, write a wrapper class to perform the search. Below the search_handle class, add a file_search class:

    class file_search 
{
search_handle handle;
string search;
public:
file_search(const char* str) : search(str) {}
file_search(const string& str) : search(str) {}
};

This class is created with the search criteria, and we have the option of passing a C or C++ string. The class has a search_handle data member, and, since the default destructor will call the destructor of member objects, we do not need to provide a destructor ourselves. However, we will add a close method so that a user can explicitly release resources. Furthermore, so that users of the class can determine the search path, we need an accessor. At the bottom of the class, add the following:

    const char* path() const { return search.c_str(); } 
void close() { handle.close(); }

We do not want instances of the file_search object to be copied because that would mean two copies of the search handle. You could delete the copy constructor and assignment operator, but there is no need. Try this: in the main function, add this test code (it does not matter where):

    file_search f1(""); 
file_search f2 = f1;

Compile the code. You'll get an error and an explanation:

    error C2280: 'file_search::file_search(file_search &)':  attempting to reference a deleted function
note: compiler has generated 'file_search::file_search' here

Without a copy constructor, the compiler will generate one (this is the second line). The first line is a bit odd because it is saying that you are trying to call a deleted method that the compiler has generated! In fact, the error is saying that the generated copy constructor is attempting to copy the handle data member and the search_handle copy constructor that has been deleted. Thus you are protected against copying file_search objects without adding any other code. Delete the test lines you just added.

Next add the following lines to the bottom of the main function. This will create a file_search object and print out information to the console.

    file_search files(argv[1]); 
cout << "searching for " << files.path() << endl;

Then you need to add code to perform the search. The pattern used here will be a method that has an out parameter and returns a bool. If a call to the method succeeds, then the file found will be returned in the out parameter and the method will return true. If the call fails, then the out parameter is left untouched and the method returns false. In the public section of the file_search class, add this function:

    bool next(string& ret) 
{
_finddata_t find{};
if (!handle)
{
handle = _findfirst(search.c_str(), &find);
if (!handle) return false;
}
else
{
if (-1 == _findnext(handle, &find)) return false;
}

ret = find.name;
return true;
}

If this is the first call to this method, then handle will be invalid and so _findfirst is called. This will fill a _finddata_t structure with the results of the search and return an intptr_t value. The search_handle object data member is assigned to this value returned from this function, and if _findfirst returns -1, the method returns false. If the call is successful, then the out parameter (a reference to a string) is initialized using a C string pointer in the _finddata_t structure.

If there are more files that match the pattern, then you can call the next function repeatedly, and on these subsequent calls the _findnext function is called to get the next file. In this case the search_handle object is passed to the function and there is an implicit conversion to intptr_t through the class's conversion operator. If the _findnext function returns -1, it means there are no more files in the search.

At the bottom of the main function, add the following lines to perform the search:

    string file; 
while (files.next(file))
{
cout << file << endl;
}

Now you can compile the code and run it with a search criterion. Bear in mind that this is constrained by the facilities of the _findfirst/_findnext functions, so the searches you can do will be quite simple. Try running this at the command line with a parameter to search for the subfolders in the Beginning_C++ folder:

    search Beginning_C++Ch*

This will give a list of the subfolders starting with Ch. Since there is no reason for search_handle to be a separate class, move the entire class to the private section of the search_handle, above the declaration of the handle data member. Compile and run the code.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset