Click here to Skip to main content
13,095,921 members (52,723 online)
Click here to Skip to main content
Add your own
alternative version


225 bookmarked
Posted 27 Sep 2004

Smart Pointers to boost your code

, 27 Sep 2004
Rate this:
Please Sign up or sign in to vote.
A beginner's introduction to the smart pointers provided by the boost library.


Smart Pointers can greatly simplify C++ development. Chiefly, they provide automatic memory management close to more restrictive languages (like C# or VB), but there is much more they can do.

I already know smart pointers, but why should I use boost?

What are Smart Pointers?

The name should already give it away:

A Smart Pointer is a C++ object that acts like a pointer, but additionally deletes the object when it is no longer needed.

"No longer needed" is hard to define, since resource management in C++ is very complex. Different smart pointer implementations cover the most common scenarios. Of course, different tasks than just deleting the object can be implemented too, but these applications are beyond the scope of this tutorial.

Many libraries provide smart pointer implementations with different advantages and drawbacks. The samples here use the BOOST library, a high quality open source template library, with many submissions considered for inclusion in the next C++ standard.

Boost provides the following smart pointer implementations:

shared_ptr<T>pointer to T" using a reference count to determine when the object is no longer needed. shared_ptr is the generic, most versatile smart pointer offered by boost.
scoped_ptr<T>a pointer automatically deleted when it goes out of scope. No assignment possible, but no performance penalties compared to "raw" pointers
intrusive_ptr<T>another reference counting pointer. It provides better performance than shared_ptr, but requires the type T to provide its own reference counting mechanism.
weak_ptr<T>a weak pointer, working in conjunction with shared_ptr to avoid circular references
shared_array<T>like shared_ptr, but access syntax is for an Array of T
scoped_array<T>like scoped_ptr, but access syntax is for an Array of T

Let's start with the simplest one:

The first: boost::scoped_ptr<T>

scoped_ptr is the simplest smart pointer provided by boost. It guarantees automatic deletion when the pointer goes out of scope.

A note on the samples:

The samples use a helper class, CSample, that prints diagnostic messages when it it constructed, assigned, or destroyed. Still it might be interesting to step through with the debugger. The sample includes the required parts of boost, so no additional downloads are necessary - but please read the boost installations notes, below.

The following sample uses a scoped_ptr for automatic destruction:

Using normal pointersUsing scoped_ptr

void Sample1_Plain()
  CSample * pSample(new CSample);

  if (!pSample->Query() )
  // just some function...
    delete pSample;

  delete pSample;

#include "boost/smart_ptr.h"

void Sample1_ScopedPtr()
             samplePtr(new CSample);

  if (!samplePtr->Query() )
  // just some function...



Using "normal" pointers, we must remember to delete it at every place we exit the function. This is especially tiresome (and easily forgotten) when using exceptions. The second example uses a scoped_ptr for the same task. It automatically deletes the pointer when the function returns 8 even in the case of an exception thrown, which isn't even covered in the "raw pointer" sample!)

The advantage is obvious: in a more complex function, it's easy to forget to delete an object. scoped_ptr does it for you. Also, when dereferencing a NULL pointer, you get an assertion in debug mode.

use forautomatic deletion of local objects or class members1, Delayed Instantiation, implementing PIMPL and RAII (see below)
not good forelement in an STL container, multiple pointers to the same object
performance:scoped_ptr adds little (if any) overhead to a "plain" pointer, it performs
  1. For this purpose, using scoped_ptr is more expressive than the (easy to misuse and more complex) std::auto_ptr: using scoped_ptr, you indicate that ownership transfer is not intended or allowed.

Reference counting pointers

Reference counting pointers track how many pointers are referring to an object, and when the last pointer to an object is destroyed, it deletes the object itself, too.

The "normal" reference counted pointer provided by boost is shared_ptr (the name indicates that multiple pointers can share the same object). Let's look at a few examples:

[__b__]void Sample2_Shared()
  // (A) create a new CSample instance with one reference
  boost::shared_ptr<CSample> mySample(new CSample); 
  printf("The Sample now has %i references\n", mySample.use_count()); // should be 1

  // (B) assign a second pointer to it:
  boost::shared_ptr<CSample> mySample2 = mySample; // should be 2 refs by now
  printf("The Sample now has %i references\n", mySample.use_count());

  // (C) set the first pointer to NULL
  printf("The Sample now has %i references\n", mySample2.use_count());  // 1

  // the object allocated in (1) is deleted automatically
  // when mySample2 goes out of scope

Line (A) creates a new CSample instance on the heap, and assigns the pointer to a shared_ptr, mySample. Things look like this:

Then, we assign it to a second pointer mySample2. Now, two pointers access the same data:

We reset the first pointer (equivalent to p=NULL for a raw pointer). The CSample instance is still there, since mySample2 holds a reference to it:

Only when the last reference, mySample2, goes out of scope, the CSample is destroyed with it:

Of course, this is not limited to a single CSample instance, or two pointers, or a single function. Here are some use cases for a shared_ptr.

  • use in containers
  • using the pointer-to-implementation idiom (PIMPL)
  • Resource-Acquisition-Is-Initialization (RAII) idiom
  • Separating Interface from Implementation

Note: If you never heard of PIMPL (a.k.a. handle/body) or RAII, grab a good C++ book - they are important concepts every C++ programmer should know. Smart pointers are just one way to implement them conveniently in certain cases - discussing them here would break the limits of this article.

Important Features

The boost::shared_ptr implementation has some important features that make it stand out from other implementations:

  • shared_ptr<T> works with an incomplete type:

    When declaring or using a shared_ptr<T>, T may be an "incomplete type". E.g., you do only a forward declaration using class T;. But do not yet define how T really looks like. Only where you dereference the pointer, the compiler needs to know "everything".

  • shared_ptr<T> works with any type:

    There are virtually no requirements towards T (such as deriving from a base class).

  • shared_ptr<T> supports a custom deleter

    So you can store objects that need a different cleanup than delete p. For more information, see the boost documentation.

  • Implicit conversion:

    If a type U * can be implicitly converted to T * (e.g., because T is base class of U), a shared_ptr<U> can also be converted to shared_ptr<T> implicitly.

  • shared_ptr is thread safe

    (This is a design choice rather than an advantage, however, it is a necessity in multithreaded programs, and the overhead is low.)

  • Works on many platforms, proven and peer-reviewed, the usual things.

Example: Using shared_ptr in containers

Many container classes, including the STL containers, require copy operations (e.g., when inserting an existing element into a list, vector, or container). However, when this copy operations are expensive (or are even unavailable), the typical solution is to use a container of pointers:

std::vector<CMyLargeClass *> vec;
vec.push_back( new CMyLargeClass("bigString") );

However, this throws the task of memory management back to the caller. We can, however, use a shared_ptr:

typedef boost::shared_ptr<CMyLargeClass>  CMyLargeClassPtr;
std::vector<CMyLargeClassPtr> vec;
vec.push_back( CMyLargeClassPtr(new CMyLargeClass("bigString")) );

Very similar, but now, the elements get destroyed automatically when the vector is destroyed - unless, of course, there's another smart pointer still holding a reference. Let's have a look at sample 3:

[__b__]void Sample3_Container()
  typedef boost::shared_ptr<CSample> CSamplePtr;

  // (A) create a container of CSample pointers:
  std::vector<CSamplePtr> vec;

  // (B) add three elements
  vec.push_back(CSamplePtr(new CSample));
  vec.push_back(CSamplePtr(new CSample));
  vec.push_back(CSamplePtr(new CSample));

  // (C) "keep" a pointer to the second: 
  CSamplePtr anElement = vec[1];

  // (D) destroy the vector:

  // (E) the second element still exists
  printf("done. cleanup is automatic\n");

  // (F) anElement goes out of scope, deleting the last CSample instance

What you absolutely must know to use boost smart pointers correctly

A few things can go wrong with smart pointers (most prominent is an invalid reference count, which deletes the object too early, or not at all). The boost implementation promotes safety, making all "potentially dangerous" operations explicit. So, with a few rules to remember, you are safe.

There are a few rules you should (or must) follow, though:

Rule 1: Assign and keep - Assign a newly constructed instance to a smart pointer immediately, and then keep it there. The smart pointer(s) now own the object, you must not delete it manually, nor can you take it away again. This helps to not accidentally delete an object that is still referenced by a smart pointer, or end up with an invalid reference count.

Rule 2: a _ptr<T> is not a T * - more correctly, there are no implicit conversions between a T * and a smart pointer to type T.

This means:

  • When creating a smart pointer, you explicitly have to write ..._ptr<T> myPtr(new T)
  • You cannot assign a T * to a smart pointer
  • You cannot even write ptr=NULL. Use ptr.reset() for that.
  • To retrieve the raw pointer, use ptr.get(). Of course, you must not delete this pointer, or use it after the smart pointer it comes from is destroyed, reset or reassigned. Use get() only when you have to pass the pointer to a function that expects a raw pointer.
  • You cannot pass a T * to a function that expects a _ptr<T> directly. You have to construct a smart pointer explicitly, which also makes it clear that you transfer ownership of the raw pointer to the smart pointer. (See also Rule 3.)
  • There is no generic way to find the smart pointer that "holds" a given raw pointer. However, the boost: smart pointer programming techniques illustrate solutions for many common cases.

Rule 2: No circular references - If you have two objects referencing each other through a reference counting pointer, they are never deleted. boost provides weak_ptr to break such cycles (see below).

Rule 3: no temporary shared_ptr - Do not construct temporary shared_ptr to pass them to functions, always use a named (local) variable. (This makes your code safe in case of exceptions. See the boost: shared_ptr best practices for a detailed explanation.)

Cyclic References

Reference counting is a convenient resource management mechanism, it has one fundamental drawback though: cyclic references are not freed automatically, and are hard to detect by the computer. The simplest example is this:

struct CDad;
struct CChild;

typedef boost::shared_ptr<CDad>   CDadPtr;
typedef boost::shared_ptr<CChild> CChildPtr;

struct CDad : public CSample
   CChildPtr myBoy;

struct CChild : public CSample
  CDadPtr myDad;

// a "thing" that holds a smart pointer to another "thing":

CDadPtr   parent(new CDadPtr); 
CChildPtr child(new CChildPtr);

// deliberately create a circular reference:
parent->myBoy = child; 
child->myDad = dad;

// resetting one ptr...

parent still references the CDad object, which itself references the CChild. The whole thing looks like this:

If we now call dad.reset(), we lose all "contact" with the two objects. But this leaves both with exactly one reference, and the shared pointers see no reason to delete either of them! We have no access to them anymore, but they mutually keep themselves "alive". This is a memory leak at best; in the worst case, the objects hold even more critical resources that are not released correctly.

The problem is not solvable with a "better" shared pointer implementation (or at least, only with unacceptable overhead and restrictions). So you have to break that cycle. There are two ways:

  1. Manually break the cycle before you release your last reference to it
  2. When the lifetime of Dad is known to exceed the lifetime of Child, the child can use a normal (raw) pointer to Dad.
  3. Use a boost::weak_ptr to break the cycle.

Solutions (1) and (2) are no perfect solutions, but they work with smart pointer libraries that do not offer a weak_ptr like boost does. But let's look at weak_ptr in detail:

Using weak_ptr to break cycles

Strong vs. Weak References:

A strong reference keeps the referenced object alive (i.e., as long as there is at least one strong reference to the object, it is not deleted). boost::shared_ptr acts as a strong reference. In contrast, a weak reference does not keep the object alive, it merely references it as long as it lives.

Note that a raw C++ pointer in this sense is a weak reference. However, if you have just the pointer, you have no ability to detect whether the object still lives.

boost::weak_ptr<T> is a smart pointer acting as weak reference. When you need it, you can request a strong (shared) pointer from it. (This can be NULL if the object was already deleted.) Of course, the strong pointer should be released immediately after use. In the above sample, we can decide to make one pointer weak:

struct CBetterChild : public CSample
  weak_ptr<CDad> myDad;

  void BringBeer()
    shared_ptr<CDad> strongDad = myDad.lock(); // request a strong pointer
    if (strongDad)                      // is the object still alive?
    // strongDad is released when it goes out of scope.
    // the object retains the weak pointer

See the Sample 5 for more.

intrusive_ptr - lightweight shared pointer

shared_ptr offers quite some services beyond a "normal" pointer. This has a little price: the size of a shared pointer is larger than a normal pointer, and for each object held in a shared pointer, there is a tracking object holding the reference count and the deleter. In most cases, this is negligible.

intrusive_ptr provides an interesting tradeoff: it provides the "lightest possible" reference counting pointer, if the object implements the reference count itself. This isn't so bad after all, when designing your own classes to work with smart pointers; it is easy to embed the reference count in the class itself, to get less memory footprint and better performance.

To use a type T with intrusive_ptr, you need to define two functions: intrusive_ptr_add_ref and intrusive_ptr_release. The following sample shows how to do that for a custom class:

#include "boost/intrusive_ptr.hpp"

// forward declarations
class CRefCounted;

namespace boost
    void intrusive_ptr_add_ref(CRefCounted * p);
    void intrusive_ptr_release(CRefCounted * p);

// My Class
class CRefCounted
    long    references;
    friend void ::boost::intrusive_ptr_add_ref(CRefCounted * p);
    friend void ::boost::intrusive_ptr_release(CRefCounted * p);

    CRefCounted() : references(0) {}   // initialize references to 0

// class specific addref/release implementation
// the two function overloads must be in the boost namespace on most compilers:
namespace boost
 inline void intrusive_ptr_add_ref(CRefCounted * p)
    // increment reference count of object *p

 inline void intrusive_ptr_release(CRefCounted * p)
   // decrement reference count, and delete object when reference count reaches 0
   if (--(p->references) == 0)
     delete p;
} // namespace boost

This is the most simplistic (and not thread safe) implementation. However, this is such a common pattern, that it makes sense to provide a common base class for this task. Maybe another article ;)

scoped_array and shared_array

They are almost identical to scoped_ptr and shared_ptr - only they act like pointers to arrays, i.e., like pointers that were allocated using operator new[]. They provide an overloaded operator[]. Note that neither of them knows the length initially allocated.

Installing Boost

Download the current boost version from, and unzip it to a folder of your choice. The unzipped sources use the following structure (using my folders):

boost\the actual boost sources / headers
doc\the documentation of the current version, in HTML format
libs\libraries (not needed for
....some more odd bits and ends ("more\" has some interesting stuff)

I add this folder to the common includes of my IDE:

  • in VC6, this is Tools/Options, Directories tab, "Show Directories for... Include files",
  • in VC7, this is Tools/Options, then Projects/VC++ directories, "Show Directories for... Include files".

Since the actual headers are in the boost\ subfolder, my sources has #include "boost/smart_ptr.hpp". So everybody reading the source code knows immediately you are using boost smart pointers, not just any ones.

Note about the sample project

The sample project contains a sub folder boost\ with a selection of boost headers required. This is merely so you can download and compile the sample. You should really download the complete and most current sources (now!).

VC6: the min/max tragedy

There is a "little" problem with VC6 that makes using boost (and other libraries) a bit problematic out of the box.

The Windows header files define macros for min and max, and consequently, these respective functions are missing from the (original) STL implementation. Some Windows libraries such as MFC rely on min/max being present. Boost, however, expects min and max in the std:: namespace. To make things worse, there is no feasible min/max template that accepts different (implicitly convertible) argument types, but some libraries rely on that.

boost tries to fix that as good as they can, but sometimes you will run into problems. If this happens, here's what I do: put the following code before the first include:

#define _NOMINMAX            // disable windows.h defining min and max as macros
#include "boost/config.hpp"  // include boosts compiler-specific "fixes"
using std::min;              // makle them globally available
using std::max;

This solution (as any other) isn't without problems either, but it worked in all cases I needed it, and it's just one place to put it.


Not enough information? More Questions?

Articles on Code Project:

Please note: While I am happy about (almost) any feedback, please do not ask boost-specific questions here. Simply put, boost experts are unlikely to find your question here (and I'm just a boost noob). Of course, if you have questions, complaints, or recommendations regarding the article or the sample project, you are welcome.


  • Sept 05, 2004: Initial version
  • Sept 27, 2004: Published to CodeProject
  • Sept 29, 2004: Minor Fixes


This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


About the Author

Germany Germany
Peter is tired of being called "Mr. Chen", even so certain individuals insist on it. No, he's not chinese.

Peter has seen lots of boxes you youngsters wouldn't even accept as calculators. He is proud of having visited the insides of a 16 Bit Machine.

In his spare time he ponders new ways of turning groceries into biohazards, or tries to coax South American officials to add some stamps to his passport.

Beyond these trivialities Peter works for Klippel[^], a small german company that wants to make mankind happier by selling them novel loudspeaker measurement equipment.

Where are you from?[^]

Please, if you are using one of my articles for anything, just leave me a comment. Seeing that this stuff is actually useful to someone is what keeps me posting and updating them.
Should you happen to not like it, tell me, too

You may also be interested in...


Comments and Discussions

GeneralThanks Pin
Ben Ratzlaff7-Jun-10 17:30
memberBen Ratzlaff7-Jun-10 17:30 
General6 years...still helping people. Pin
Peter Lindahl2-May-10 19:51
memberPeter Lindahl2-May-10 19:51 
GeneralRe: 6 years...still helping people. Pin
peterchen2-May-10 23:51
memberpeterchen2-May-10 23:51 
Generaltemporary shared_ptr question Pin
cms.shenfy27-Apr-10 17:16
membercms.shenfy27-Apr-10 17:16 
GeneralRe: temporary shared_ptr question Pin
peterchen27-Apr-10 20:20
memberpeterchen27-Apr-10 20:20 
GeneralAnother question on "cyclic" [modified] Pin
Brian Bennett13-Feb-10 12:16
memberBrian Bennett13-Feb-10 12:16 
GeneralRe: Another question on "cyclic" Pin
mr0207723-Dec-10 4:58
membermr0207723-Dec-10 4:58 
GeneralQuestion on "cyclic references" Pin
minkowski7-May-09 23:16
memberminkowski7-May-09 23:16 
GeneralRe: Question on "cyclic references" Pin
peterchen8-May-09 10:19
memberpeterchen8-May-09 10:19 
GeneralRe: Question on "cyclic references" Pin
minkowski9-May-09 10:22
memberminkowski9-May-09 10:22 
GeneralOne more on my personal fight against auto_ptr Pin
peterchen25-Jan-09 0:08
memberpeterchen25-Jan-09 0:08 
GeneralPIMPL and scoped_ptr [modified] Pin
Moak20-Mar-07 8:40
memberMoak20-Mar-07 8:40 
GeneralUsing shared ptr in classes... Pin
Bernhard18-Dec-05 21:50
memberBernhard18-Dec-05 21:50 
GeneralRe: Using shared ptr in classes... Pin
peterchen25-Jan-09 0:11
memberpeterchen25-Jan-09 0:11 
GeneralFollowup Article Pin
peterchen7-Oct-04 5:49
memberpeterchen7-Oct-04 5:49 
GeneralBoost smart pointers are not thread safe Pin
Assar Brynlund5-Oct-04 19:52
memberAssar Brynlund5-Oct-04 19:52 
The reference counter is open for race conditions. E.g., if you delete the last and second last smart pointers to an object, the object will be delete zero, one or two times. And if you create and/or delete smart pointers simultaneously in parallel threads, the reference counter value may be corrupted.

Access to the pointed to data is allowed, but will cause clashes for any non-atomic operations, such as assignment to non-primitive types or sequences of operations on primitive types.

::boost::shared_ptr<int> p_int(new int(0));
::boost::shared_ptr<int> p2_int(p_int);

// Start using p2_int in a parallel thread.

if (10 == *p_int) {
*p_int = 20; // *p_int may no longer be 10!
GeneralBoost smart pointers *ARE* thread safe Pin
peterchen5-Oct-04 21:02
memberpeterchen5-Oct-04 21:02 
GeneralRe: Boost smart pointers are not thread safe Pin
Joaquín M López Muñoz5-Oct-04 21:03
memberJoaquín M López Muñoz5-Oct-04 21:03 
GeneralRe: Boost smart pointers are not thread safe Pin
Anonymous1-Jan-05 12:19
sussAnonymous1-Jan-05 12:19 
GeneralRe: Boost smart pointers are not thread safe [modified] Pin
John M. Drescher13-Jul-07 4:03
memberJohn M. Drescher13-Jul-07 4:03 
GeneralRe: Boost smart pointers are not thread safe Pin
Member 25183465-Apr-09 10:44
memberMember 25183465-Apr-09 10:44 
Generalsmart pointers are as thread safe as raw pointers. Pin
peterchen5-Apr-09 13:44
memberpeterchen5-Apr-09 13:44 
GeneralRe: smart pointers are as thread safe as raw pointers. Pin
Member 25183465-Apr-09 19:28
memberMember 25183465-Apr-09 19:28 
GeneralRe: smart pointers are as thread safe as raw pointers. Pin
peterchen5-Apr-09 20:36
memberpeterchen5-Apr-09 20:36 
GeneralRe: smart pointers are as thread safe as raw pointers. Pin
Member 25183466-Apr-09 4:29
memberMember 25183466-Apr-09 4:29 
GeneralRe: smart pointers are as thread safe as raw pointers. Pin
peterchen6-Apr-09 20:36
memberpeterchen6-Apr-09 20:36 
GeneralRe: smart pointers are as thread safe as raw pointers. Pin
Member 25183467-Apr-09 7:36
memberMember 25183467-Apr-09 7:36 
GeneralVery good job! Pin
Rommel5-Oct-04 7:08
memberRommel5-Oct-04 7:08 
QuestionGood article - what about loki ? Pin
Simon Hughes5-Oct-04 2:48
memberSimon Hughes5-Oct-04 2:48 
AnswerRe: Good article - what about loki ? Pin
peterchen5-Oct-04 5:07
memberpeterchen5-Oct-04 5:07 
GeneralAn observation (again) about the convenience of a Boost-devoted forum Pin
Joaquín M López Muñoz4-Oct-04 10:10
memberJoaquín M López Muñoz4-Oct-04 10:10 
Generalgot some issue with the scoped_ptr Pin
f21-Oct-04 4:32
memberf21-Oct-04 4:32 
GeneralRe: got some issue with the scoped_ptr Pin
ilmcuts1-Oct-04 5:38
memberilmcuts1-Oct-04 5:38 
GeneralRe: got some issue with the scoped_ptr Pin
peterchen1-Oct-04 11:22
memberpeterchen1-Oct-04 11:22 
GeneralRe: got some issue with the scoped_ptr Pin
f21-Oct-04 22:24
memberf21-Oct-04 22:24 
GeneralRe: got some issue with the scoped_ptr Pin
peterchen1-Oct-04 22:43
memberpeterchen1-Oct-04 22:43 
GeneralRe: got some issue with the scoped_ptr Pin
f21-Oct-04 23:42
memberf21-Oct-04 23:42 
GeneralIndeed a great article! Pin
Jörgen Sigvardsson28-Sep-04 9:28
memberJörgen Sigvardsson28-Sep-04 9:28 
GeneralGood stuff but.. Pin
McClamm28-Sep-04 9:01
memberMcClamm28-Sep-04 9:01 
GeneralRe: Good stuff but.. Pin
Nemanja Trifunovic28-Sep-04 9:36
memberNemanja Trifunovic28-Sep-04 9:36 
GeneralRe: Good stuff but.. Pin
peterchen28-Sep-04 10:12
memberpeterchen28-Sep-04 10:12 
GeneralRe: Good stuff but.. Pin
McClamm28-Sep-04 10:32
memberMcClamm28-Sep-04 10:32 
GeneralRe: Good stuff but.. Pin
Andrew Walker28-Sep-04 16:32
memberAndrew Walker28-Sep-04 16:32 
GeneralRe: Good stuff but.. Pin
Martin Duncan1-Jan-07 16:14
memberMartin Duncan1-Jan-07 16:14 
GeneralRe: Good stuff but.. Pin
Rohit Joshi30-Sep-04 10:49
memberRohit Joshi30-Sep-04 10:49 
General5 Pin
Nemanja Trifunovic28-Sep-04 2:15
memberNemanja Trifunovic28-Sep-04 2:15 
Generalexcellent... Pin
waxie28-Sep-04 1:40
memberwaxie28-Sep-04 1:40 
GeneralGood one Pin
Giannakakis Kostas27-Sep-04 19:59
memberGiannakakis Kostas27-Sep-04 19:59 
GeneralRe: Good one Pin
peterchen27-Sep-04 20:46
memberpeterchen27-Sep-04 20:46 
GeneralRe: Good one Pin
Giannakakis Kostas28-Sep-04 19:59
memberGiannakakis Kostas28-Sep-04 19:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.170813.1 | Last Updated 27 Sep 2004
Article Copyright 2004 by peterchen
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid