Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Function-like Macros vs. Inline Functions

0.00/5 (No votes)
25 Sep 2004 2  
A write up that tries to highlight some of the pros and cons of both these exceptional features provided in the object oriented arena of C++.

Introduction

�The question of whether computers can think is like the question of whether submarines can swim.� - Edsger Dijkstra

Computers or rather compilers, for us software folks, cannot think totally on its own. The obligation is thus on us, to furnish the best information to the compilers to derive the best out of them. A common instance where we tend to misfire is in the use of Function-like Macros and Inline Functions. This write up is to try and highlight some of the pros and cons of both these exceptional features provided in the object oriented arena of C++.

Though most of us are very much aware of what function-like macros and inline functions are, let us revisit the same to brush up our thoughts.

Function-like Macros

Macros are most frequently used to define names for constants that occur repeatedly in a program. The more preferred way of defining these constants is by using the keyword const. Since the current focus is not on which is the best way to define constants, let us leave it alone. The #define directive is quite powerful and hence allows the macro name to have arguments and thus behave like a function. Such forms of macros are referred to as function-like macros. Each time the macro name is encountered with arguments, the arguments used in its definition are replaced by the actual arguments found.

For example:

#include "iostream.h"

 
#define MAX(a, b)    ((a < b) ? b : a)
 
int main( void)
{
    cout << "Maximum of 10 and 20 is " << MAX(10, 20) << endl;
    return 0;
}
Output:
Maximum of 10 and 20 is 20

On compilation of the program, the "a" and "b" in the macro definition will be replaced by 10 and 20 respectively. Though this looks like a piece of cake, it has its own potholes which we will visit in the later sections.

Inline Functions

Inline functions are an imperative feature of C++ and are frequently used with classes. These are no different from the normal functions except for the fact that they are never actually called. These functions are, as their name suggests, expanded in line at each point of invocation. All that needs to be done to enable or cause a function to expand at the point of invocation is to add the inline keyword before the function definition.

For example:

#include "iostream.h"

using namespace std;
 
inline int max(int a, int b)
{
    return a < b ? b : a;
}
int main()
{
    cout << "Maximum of 10 and 20 is " << max(10, 20) << endl;
    return 0;
}
Output:
Maximum of 10 and 20 is 20

This above code as seen by the compiler would resemble the below:

#include �iostream.h�

using namespace std;
 
int main()
{
    cout << "Maximum of 10 and 20 is " << (10> 20 ? 10 : 20) << endl;
    return 0;
}

This seems so similar to the function-like macros, isn�t it? Hold on till we get into the depth.

Function-like Macros vs. Inline Functions

Macros are more often used to define constants that can be effectively used instead of const or non const variables. Since the macros are interpreted at compile time, they gain the advantage of being non replaceable.

Function-like macros pack more surprises than one can imagine and have to be very careful.

Function-like macros are immensely beneficial when the same block of code needs to be executed umpteen numbers of times. For example, suppose one needs to make entries to a log file, function-like macros will be of great help.

Consider the below example:

#include "iostream.h"

 
#define LOG(X) WriteToFile((X), __FILE__, __LINE__ )

WriteToFile is a function. Now, one can use this macro to write the details in the log file along with a message, file name and line number as depicted below:

int main()
{
        LOG("Inside Main");
        return 0;
}

Though not wanting to sound biased, one cannot help but view function-like macros with some wariness in spite of the great deal of advantages that they present. Function-like macros when used always expand at the point of usage. The function-like macros are pre-processed at compile time, and as a result, there is really no macro existing at runtime. This is why there is an increase in the size of the binary files.

Yet another major setback of the function-like macros is the lack of type checking leading to a high level of type insecurity. The arguments passed on to a macro are never preprocessed. Looks totally harmless, doesn�t it?

This can cause a whole lot of chaos.

Consider the example below which depicts the disadvantage of the lack of type checking. In this example, the result is always the value passed in as the second argument.

#include "iostream.h"

 
#define MAX(a, b)              ((a < b) ? b : a)
 
int main( void)
{
    cout << "Maximum of 10 and 20 is " << MAX("20", "10") << endl;
    return 0;
}
Output:
Maximum of 10 and 20 is 10

Function-like macros when combined with conditional statements need to be very carefully handled. Take a look at the example below:

#include "iostream.h"

 
#define MAX(a, b)       \
        if (a < b)                  \
                cout << "Maximum is b:" << b << endl;
 
int main( void)
{
        if (true)
                MAX(20, 10)
        else
                    cout << "Macro failed." << endl;
        return 0;
}
Output:
Macro failed.

This will always give �Macro failed.�. This is because the compiler will look at the code as shown below:

#include "iostream.h"

 
int main( void)
{
    if (true)
        if (20 < 10) 
            cout << "Maximum is b:" << b << endl;
        else
            cout << "Macro failed." << endl;
    return 0;
}

A similar combination which could tamper the result is that of the parenthesis used within the function-like macros. Remember the very first example that was used to depict the use of a macro. Now, when we remove the enclosing braces of the statement, the output of the macro goes completely berserk. It is always going to give the result of the conditional statement.

#include "iostream.h"

 
#define MAX(a, b)    ((a < b) ? b : a)
 
int main( void)
{
    cout << "Maximum of 10 and 20 is " << MAX(10, 20) << endl;
    return 0;
}
Output:
Maximum of 10 and 20 is 1

Function-like macros need additional handling of arguments that have side effects. Expressions supplied as arguments may not always be evaluated before entering the body of the function-like macros. The increment or decrement operators when passed as arguments would behave as depicted in the subsequent example.

#include "iostream.h"

 
#define MAX(a, b)     cout << "The values are - a:" << a << " b:" << b << endl;
 
int main( void)
{
    int a = 10;
    int b = 10;
    MAX(a++, b++);
    return 0;
}
Output:
The values are - a:10 b:10

Function-like macros, notwithstanding the shortcomings, still have their own benefits, and one cannot totally term as hazardous. This is well augmented by the extensive use of function-like macros in MFC.

The point where inline functions primarily score over the function-like macros is perhaps the feasibility to step through the code. This perhaps is the one single motivation that causes developers to be biased if at all towards inline functions.

Inline functions when used effectively increase the performance but then nothing comes for free. The enhanced performance will be at the cost of increased compile time and possibly size. Inline is nothing but a request to the compiler. The onus shifts to the compiler which decides on the optimization once the request has been issued. The compiler would then decide upon the optimization. So, the focus when using inline functions should be on the cost and the benefits, since these are directly coupled with the expansion.

Inline functions, though declared that way, are not always inlined. It is not always necessary to declare a function as inline. Any function whose body is defined within the class body will be implicitly treated as inline. But there is a catch to this. The implicit treatment depends on the complexity and length of the function. Remember who is the boss when it comes to inline functions? Inline is just a request and it is the compiler who is vested with the authority to accept or reject the inline request.

In the ensuing example, there are two functions of which one has been explicitly declared as inline while the other isn�t. In this case, both are treated by the compiler as inline since their definition is within the class body.

class CInlinesExamples
{
public:
    void ImplicitInline()
    {  cout << "This is an implicit inline function." << endl; }
 
    inline void ExplicitlyInline()
    {  cout << "This is an explicit inline function." << endl; }
};

Any function will have to be declared explicitly as inline when the following style is used while writing the code:

class CInlinesExamples
{
public:
    inline void InlineFunction();
};
 
void CInlinesExamples:: InlineFunction()
{
    cout << " This is an explicit inline function." << endl; 
}

The inline directive will be totally of no use when used for functions which are:

  • recursive
  • long
  • composed of loops

In a class, generally the constructor, copy constructor, destructor, and assignment operator overloading are all inline by default.

Inline functions are always on the verge. They might be good or bad, really bad. Let us see why.

Inline functions might make the process smaller and/or faster. The compiler often generates a lot of low level code to push and pop registers or parameters. This generally is more than the code required for inline-expanding the function's body. This happens with all functions, irrespective of the size. The optimizer is able to remove a lot of redundant code through procedural integration - that is, when the optimizer is able to make the large function small. This procedural integration will remove a bunch of unnecessary instructions, which might make the whole process run faster.

Inline functions might make the process larger and/or slower. A process with a number of inline functions will end up having a large code size. This might cause "thrashing� - a computer activity that makes little or no progress, as a result of memory or other resources becoming exhausted or too limited to perform needed operations - on demand-paged virtual-memory systems. To put it in other words, an increase in the executable size may result in more disk operation as the system will end up spending most of its time fetching the next chunk of code from the disk.

For example, if a system has 100 inline functions, each of this expands to 100 bytes of executable code and is called in 100 places, that�s an increase of 1MB. Is that 1MB going to cause problems? Who knows, but it is possible that, that last 1MB could cause the system to "thrash," and that could slow things down.

Contradictorily, inline functions while leading to thrashing may reduce the page faults. The working set size - number of pages that need to be in memory at once - might go down even if the executable size goes up.

Consider the example below:

class CInlinesExamples
{
public:
    void OuterFunction()
    {  cout << "This function is the caller function." << endl; }

    void InnerFunction()
    {  cout << "This function is the called function." << endl; }
};

When the InnerFunction is called within the OuterFunction, the code is often on two distinct pages; when the compiler procedurally integrates the code of the InnerFunction into the OuterFunction, the code is often on the same page. This will reduce the possibility of any page faults.

In today�s scenarios, a majority of the systems developed are not CPU-bound but inadvertently either I/O-bound, database-bound, or network-bound. This means that overall performance of the system is more dependent on the file system, the database, or the network, thus making them the bottleneck. This renders the inline function effectively insignificant unless they are used within the bottleneck. They may not be of any relevance to the speed or performance under such circumstances.

If function-like macros were a pack of surprises then the inline functions are the ultimate riddles. There are simply no simple answers as to when or why to use them. The best way to know is to play with it to see what is best.

Finale

To wrap up on which of the two is better, one cannot help but be diplomatic. Though there seems to be a lot of pros and cons for the usage of both function-like macros and inline functions, the truth is that they both do make difference in their own way.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here