简体   繁体   中英

Is there any advantage to implementing functions as free functions rather than members in C++?

I'm interested in the technical logistics. Is there any advantage, such as memory saved, etc., to implementing certain functions dealing with a class?

In particular, implementing operator overloads as free functions (providing you don't need access to any private members, and even then you can make them use a friend non-member)?

Is a distinct memory address provided for each function of the class each time an object is created?

This answer may helps you : Operator overloading : member function vs. non-member function? . In general free functions are mandatory if you need to implement operators on classes you don't have access to code source (think about stream s) or if left operand is not of class type ( int for example). If you control the code of the class then you can freely use function members.

For your last question, no, function members are uniquely defined and an object internal table is used to point to them. Function members can be viewed as free functions with an hidden parameter that is a pointer to the object, ie of(a) is more or less the same as f(&o,a) with a prototype roughly like f(C *this,A a); .

There are various articles about circumstances when implementing functionality using non-member functions is preferred over function members.

Examples include

Scott Meyers (author of books like "Effective C++", "Effective STL", and others) on how non-members improve encapsulation: http://www.drdobbs.com/cpp/how-non-member-functions-improve-encapsu/184401197

Herb Sutter in his Guru of the Week series #84 "Monoliths Unstrung". Essentially he advocates, when it is possible to implement functionality as a member or as a non-member non-friend, prefer the non-member option. http://www.gotw.ca/gotw/084.htm

Non- static member functions have an implicit this parameter. If your function doesn't use any non- static members, it should be either a free function or a static member function, depending on what namespace you want it to be in . This will avoid confusion for human readers (who will be scratching their heads looking for the reason it's not static ), and will be a small improvement in code size, with a probably non-measurable gain in performance.

To be clear: in the asm, there's zero difference between a static member function and a non-member function . The choice between static-member and global or static (file scope) is purely a namespace / design quality issue, not a performance issue. (In Unix shared libraries (position-independent code), calling global functions has a level of indirection through the PLT, so prefer static file-scope functions. This is a different meaning of the static keyword vs. global static -member functions, which are globally visible and thus subject to symbol interposition .)


One possible exception to this rule is that wrapper functions that pass on most of their args unchanged to another function benefit from having their args in the same order as the function they call, so they don't have to move them between registers. eg if a member function does something simple to a class member and then calls a static member function with the same arg list, it's actually without the implicit this pointer, so all the args have to move over by one register.


Most ABIs use an args-in-registers calling convention. 32bit x86 (other than some Windows calling conventions) is the major exception I know of, where all args are always passed on the stack. 64bit x86 passes the first 6 integer args in registers, and the first 8 FP args in xmm registers ( SysV ). Or the first 4 args of args of either type in registers (Windows).

Passing an object pointer will typically take an extra instruction or two at every call site. If the implicit first arg bumps any other args out of the limited set of arg-passing regs, then it will have to be passed on the stack. This adds a few cycles of latency to the critical path involving that arg for the store-load round trip, and extra instructions in the callee as well as the caller. (See the wiki for links to more details about this sort of thing, for that platform).

Inlining of course eliminates this. static functions can also be optimized by modern compilers, because the compiler knows all the calls come from code it can see, so it can make them non-standard. IDK if any compiler will drop unused args during inter-procedure optimization. Link-time and/or whole-program optimization may also be able to reduce or eliminate overhead from unused args.

Code-size always matters at least a little, since smaller binaries load from disk faster, and miss less in I-cache. I don't expect any measurable speed difference unless you specifically design an experiment that's sensitive to it.

One strictly technical difference, which also is valid for static vs non- static member functions, might affect performance in extreme scenarios:

For a member function, the this pointer will be passed as an "invisible" parameter to the function. Usually, depending on the parameter types, a fixed number of parameter values can be passed via registers instead of via the stack (registers are faster to read and write).

If the function already takes that number of parameters explicitly , then making it a non- static member function might cause parameters to be passed via the stack instead of via registers, and if that happens, barring optimizations that may or may not happen, the function call will be slower.

However, even if it is slower - in this case, in the vast majority of any use cases that you can dream up, slower is insignificant (but real).

Depending on the subject, class functions may not be the right solution. Class functions depend on the assumption that exists an assymetry between the arguments of the corrispetive non class function where it is cleary individuated a main subject of the function (id est the implicitly passed this that practically corresponds to passing the object by reference). On the other side many times such an assymetry may not exist. In those cases the free functions are the best solution. Regarding execution speed there isn't any difference because the method of a class is just a function where the first argument is the this pointer. So it is totally equivalent the the corrispetive non class function where the first element is the pointer to the object.

The most important thing to consider when designing classes is, "what is the invariant?" Classes are design to protect invariants. So, classes must be the tiniest as possible to ensure that invariant is properly protected. If you have so many member/friend functions, there is more code to review.

From this point of view, if a class has members which don't need to be protected (for example, a boolean which its corresponding get/set functions can be freely changed by the user), is better to put that attributes as public and remove the get/set functions (more or less, these are the Bjarne Stroustrup words).

So, which functions must be declared inside the class and which ones out? Inside functions must be these minimum required set of function to protect the invariant, and outside functions must be any function that can be implemented using the other ones.

The thing with operator overloading is another history, because the criteria to put some operators inside, and some other outside, is because of syntactical issues related to implicit conversions and so on:

class A
{
private:
   int i_i;

public:
   A(int i) noexcept : i_i(i) {}
   int val() const noexcept { return i_i; }

   A operator+(A const& other) const noexcept
   { return A(i_i + other.i_i); }
};

A a(5);
cout << (4 + a).val() << endl;

In this case, since the operator is defined inside the class, the compiler doesn't find the operator, because the first argument is an integer (when an operator is called, the compiler search for free functions and functions declared inside the class of the first argument).

When declared outside:

class A
{
private:
   int i_i;

public:
   A(int i) noexcept : i_i(i) {}
   int val() const noexcept { return i_i; }
};

inline A operator+(A const& first, A const& other) const noexcept;
{ return A(first.val() + other.val()); }

A a(5);
cout << (4 + a).i_i << endl;

In these case, the compiler find the operator, and try to perform an implicit conversion of the first parameter from int to A, using the proper A's constructor.

In these case, the operator can also be implemented using other functions, so, it doesn't need to be friend and you can be sure the invariant is not compromised with that additional function. So, in these concrete example, moving the operator outside is good for two reasons.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM