Jump to content
  • Advertisement
Sign in to follow this  
grill8

Questions about inlining constructors/destructors and code structure.

This topic is 3544 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hello, I have a couple of questions about inlining constructors/destructors and code structure. Question 1) Is inlining constructors/destructors still considered bad? I know it once was because of extra code that goes on behind the scenes that those methods require. Do compilers factor that out nowadays? Question 2) Say I have the following class:
[source lang = "cpp"]

class Foo
{
public: 
     Foo(const std::string& desc):m_Desc(desc){}
     const std::string& GetFoo()const{return m_Desc;}
private:
     std::string m_Desc;
}


If that class is the only class in the file then it seems silly to have to create a whole .cpp file simply for the simple constructor. On the other hand again, I have heard that having inline constructors is bad. Any help is appreciated. Jeremy

Share this post


Link to post
Share on other sites
Advertisement
I have never heard of inline constructors or destructors, specifically, as being a bad idea, and I can't think of a reason why.

Having inline functions in a header means every time a function is changed, all translation units that include the header must be recompiled. If the header only contains extremely obvious functions with no chance of changes in the future, then it's fine, but complex functions with intricate implementation details usually end up being tweaked or rewritten often enough to prompt moving them away.

Share this post


Link to post
Share on other sites
Quote:
Original post by grill8
Is inlining constructors/destructors still considered bad? I know it once was because of extra code that goes on behind the scenes that those methods require. Do compilers factor that out nowadays?

Scott Meyers says that it's not necessarily bad, but he warns that what may seem like a trivial constructor might in fact be far more complex behind the scenes, and thus not suitable for inlining. Watching the generated assembly or doing profiling are really the only ways of finding out whether it's a good idea or not for performance.

Obviously, different rules apply depending on which goal you're trying to accomplish. Inlined non-trivial functions are fine for test code, or for applications where you know that the target platform has more than the required performance capacity. Say, if you're writing a tetris clone for a modern multicore processor and a GeForce 9800 GPU, you really don't have to worry about performance and can instead fully concentrate on writing the code quickly and correctly. As you said, creating a .cpp file and un-inlining all methods can take a lot of extra time.

Share this post


Link to post
Share on other sites
Quote:
Original post by sb01234
Quote:
Original post by grill8
Is inlining constructors/destructors still considered bad? I know it once was because of extra code that goes on behind the scenes that those methods require. Do compilers factor that out nowadays?

Scott Meyers says that it's not necessarily bad, but he warns that what may seem like a trivial constructor might in fact be far more complex behind the scenes, and thus not suitable for inlining.


That is very true: always beware of constructors and destructors, because they often perform more work (or are longer) than you think due to implicit base and member calls.

For example, consider a class with a data member of type std::vector<std::string>. And consider the implications of inlining the constructor and/or the destructor of that class because they are apparently empty. If the compiler decides to follow your suggestion, you may end up with _a lot_ of code inlined --much more than what you had considered in the first place--.

Share this post


Link to post
Share on other sites
Compilers tend to ignore the inline keyword when it comes to actually inlining, which is the only effect declaring one's constructors inline is supposed to have anyways. I wouldn't fret so much over it. Start knocking things down on your profiled bottlenecks list instead :P

Share this post


Link to post
Share on other sites
One possible reason why inlining those can be considered bad is that it's a hazard when it comes to binary compatibilities with different versions of a shared library.

If you have a class in a shared library and the constructor is inline, then the client code using the library will include a version of the constructor that will become outdated if additional stuff is added into the class in a later version of the library, since this additional stuff will not be initialized by the outdated inlined constructors.

Of course, inline functions in general can be binary compatibility hazards, but since most of them are just trivial accessors they aren't as dangerous as inline constructors.

Share this post


Link to post
Share on other sites
Quote:
Original post by MaulingMonkey
Compilers tend to ignore the inline keyword when it comes to actually inlining, which is the only effect declaring one's constructors inline is supposed to have anyways. I wouldn't fret so much over it. Start knocking things down on your profiled bottlenecks list instead :P


Seconded.

Modern compilers will inline what they want, not what you ask them to. If you, for example, enable LTCG in Microsoft Visual C++, for example, your whole project, all its source files and headers, will be merged into one huge chunk of preprocessed code, so the compiler won't even know whether your constructur was in the header or in a source file.

There are some reasons you may want to avoid inlining specific code in the headers that apply to constructors and destructors as well as methods.

One has already been mentioned, if you write code in your headers, any other file that includes your header will have to be recompiled if you modify the code.

The second one would be if you're writing a C++ DLL. When using the DLL, your compiler might decide to inline stuff it sees in the headers, so when you discover a bug in one of those inlined methods of your DLL and fix it, there's no guarantee that it will fix an application by just replacing the DLL. I do think MSVC is smart about this when it sees a __declspec(dllimport), but I wouldn't rely on it, as it's a compiler-specific feature.

Share this post


Link to post
Share on other sites
Oh it happened some days ago that I wanted to have a function (which used dynamic sized arrays) work with both std::vector<> and native arrays. The minimal change I could come up with was like


template <typename T> struct etHop : public T {}; // e.g. for std::vector

template <typename T> struct etHop <T*> { // for pointer types (pointing to arrays)
T *m;
etHop (const unsigned int size) : m (reinterpret_cast <T*> (alloca (sizeof (T) * size))) {}
T & operator [] (int i) { return m ; }
const T & operator [] (int i) const { return m ; }
operator T* () { return m; }
operator T const * const () const { return m; }
};



and the function that works on both:

scalar_t operator () (const parameters_t &parameters_) const {
etHop<parameters_t> parameters (this->argumentCount); // this is the only line that really changed
for (int i=0; i<this->argumentCount; ++i) {
parameters = (*this->arguments ) (parameters_);
}
return (*this->function.lock()) (parameters); // called function has same signature as this function
}



Now it happened that this approach failed due to the alloca(), because the lifetime of memory allocated with alloca() is only valid within the current stackframe (or deeper ones), so when leaving the constructor, it became dense.

My unportable solution (*) was either A) forced inlining of etHop::etHop(), or B) making etHop::etHop() naked (i.e. no epilogue/prologue). I tried it out with A), and it all worked fine :)

A bummer that there's no forced inlining in std-c++ :|

For a compiling example, look at this:

// p-code that happens to compile ;)
#include <iostream>

struct Foo {
int *vals;
unsigned int size;
Foo (unsigned int size)
__attribute__ ((always_inline)) // tweak for other compilers
: size(size), vals (static_cast <int*> (alloca (sizeof (int) * size))) {
using namespace std;
cout << "Inside Foo::Foo()\n";
for (int i=0; i<size; vals = ++i); // 1,2,3,...
for (int i=0; i<size; ++i) cout << " [" << i << "] = " << vals << "\n"; // okay
}
int & operator [] (int i) { return vals ; }
int const & operator [] (int i) const { return vals ; }
};

int main () {
using namespace std;

int size = 5;
{
cout << "Inside main(), using int*\n";
int *vals = static_cast <int*> (alloca (sizeof (int) * size));
for (int i=0; i<size; vals = ++i); // 1,2,3,...

for (int i=0; i<size; ++i) cout << " [" << i << "] = " << vals << "\n"; // okay
cout << flush;
}
{
Foo vals (size);
cout << "Inside main(), using Foo\n";
for (int i=0; i<vals.size; vals = ++i); // 1,2,3,...

for (int i=0; i<vals.size; ++i) cout << " [" << i << "] = " << vals << "\n"; // bogus values if not inlined
for (int i=0; i<vals.size; ++i) cout << " [" << i << "] = " << vals << "\n"; // possibly different bogus values (if not inlined)
cout << flush;
}
}





(*) of course I skipped that solution because it was unportable, but still I liked the elegance :)

Share this post


Link to post
Share on other sites
That code is plain evil! (:

You actually use alloca() and store the stack allocated array for later use, relying on the users of etHop to only use it as a local variable.
And then you rely on the constructor being inlined so the stackalloc() ends up in the calling method.
And finally, you assume that the compiler accepts changing the meaning of a program by inlining alloca() and not restoring the stack after the inlined constructor code ends.

I'd prefer just putting the alloca() in plain view, even if forced inlining was possible across compilers.

If that "etHop" does more, you could still pass the pointer as preallocated memory to the constructor, eg.

void foo(size_t length) {
etHop bar(alloca(length), length);

bar.do_something();
}

The drawback is the duplication of the alloca() call.
The advantage is that it works in any compiler, in debug and release mode and "etHop" can also use heap-allocated memory now if required (eg. for large data sets, whatever it does)

Share this post


Link to post
Share on other sites
Quote:
Original post by Cygon
You actually use alloca() and store the stack allocated array for later use, relying on the users of etHop to only use it as a local variable.


I should have pasted more, my fail :S It was defined as a private member template class, and operator(const parameters_t&) was the only intended target audience.


Quote:
And then you rely on the constructor being inlined so the stackalloc() ends up in the calling method.

But I have already said this with "forced inlining" and "it's unportable" and "I am not going to use it", just that I liked it and to give an example for forced-inling :P


Quote:
And finally, you assume that the compiler accepts changing the meaning of a program by inlining alloca() and not restoring the stack after the inlined constructor code ends.


This is another view on the same (previous) statement.


Quote:
If that "etHop" does more, you could still pass the pointer as preallocated memory to the constructor, eg.


No, it's only intention was to serve as a thin wrapper to enable the same notation for native arrays and vectors, with the native arrays being stack-allocated. It does this job (except for changing the type from "parameters_t" to "etHop<parameters_t>"), basically by injecting an invisible alloca() into the caller.

Again: I won't use this code because both alloca() and forced inlining are not std-c++ (though alloca() is enabled in many compilers).

While this example was basically a fun exercise, there are areas where code really relies on inlined code.


Quote:

void foo(size_t length) {
etHop bar(alloca(length), length);
bar.do_something();
}


A) see previous arguments, that constructor is incompatible to std::vector<>
B) why use a non-standard function (it misses a typecast btw) when you can as well do it the c++ way:

//void foo (int const length) {
// int vals [length];
// ...
//}

C) in the meanwhile I have implemented B) as a template specialisation on pointer types of parameters_t

[Edited by - phresnel on January 7, 2009 10:16:05 AM]

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!