November 21st, 2025
likeintriguing2 reactions

Maybe somebody can explain to me how weak references solve the ODR problem

Some time ago, I proposed Using type aliasing to avoid the ODR problem with conditional compilation. A Reddit thread claimed that “It’s weak functions basically. You can actually achieve this without needing templates but need to separate the weak and non-weak definitions.”

I’m not seeing how weak functions help here, but maybe somebody can set me straight.

Weak functions are still functions. If you are changing data layout (as we are in our case of adding a Logger member), you can weaken any functions that access the data members, but that doesn’t help any class consumers that access those data members, because the data member accesses are inlined into the call site.

Let’s look at our Widget again.

// widget.h

struct Widget
{
    Widget();

    void SetName(std::string const& name);

    ⟦ more stuff ⟧

#ifdef EXTRA_WIDGET_DEBUGGING
    Logger m_logger;

    void Log(std::string const& message) {
        m_logger.log(message);
    }
#else
    void Log(std::string const& message) {
        // no extra logging
    }
#endif

    std::string m_name;
};

If somebody just accesses the name directly:

void sample(Widget& w)
{
    std::cout << w.m_name;
}

The access to w.m_name is inlined into the sample function. It’ll probably go something like

    mov     rsi, edi                ; rsi -> Widget
#ifdef EXTRA_WIDGET_DEBUGGING
    add     rsi, 16                 ; offset of m_name
                                    ; if there is a Logger
#else
    add     rsi, 24                 ; offset of m_name
                                    ; if there isn't a Logger
#endif
    mov     rdi, offset std::cout
    call    operator<<        ; print the name to cout

The compiler is going to inline the offset to m_name, so it will choose an offset based on whether the EXTRA_WIDGET_DEBUGGING symbol is set. But if the caller is building with EXTRA_WIDGET_DEBUGGING set the opposite way, then you are printing garbage.

The idea behind templating the Widget based on whether it is debugging or not is to allow parts of the code to use a debugging Widget, but other parts to use a non-debugging Widget.

template<bool debugging>
struct WidgetT
{
    ⟦ ... ⟧
};

#ifdef EXTRA_WIDGET_DEBUGGING
using Widget = WidgetT<true>;
#else
using Widget = WidgetT<false>;
#endif

The pieces that use a debugging widget get WidgetT<true>, and those that don’t get a WidgetT<false>.

While weak pointers let you provide two versions of a function (say, debugging and non-debugging), you can’t have both versions active at the same time. Everybody has to agree on whether it’s a debugging version or a non-debugging version.

But maybe I’m missing something. If you can explain how to accomplish this with weak functions, let me know in the comments.

Bonus chatter: Another downside of weak functions is that they aren’t inlined, but maybe link time optimization can re-inline them.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

6 comments

Discussion is closed. Login to edit/delete existing comments.

Sort by :
  • Henning Meyer

    The way I understand the referenced reddit comment:

    You can manually re-implement OOP-style extensibility on top of weak functions instead of virtual functions.

    Yes, that approach breaks if you access members directly, you need to go via functions.

    Yes, that approach comes with a performance overhead, that might be mitigated in LTO, just like virtual functions.

    Yes, you need to make sure to have a weak destructor.

    You would be better of just using inheritance and virtual functions, but it is technically possible. It would be brittle and surprising and I would shoot it down in code review.

  • kyle sylvestre

    Why does SHAddToRecentDocs restrict folders to only be shown for Windows Explorer? Snippet from the function page:

    Folders are also accepted by SHAddToRecentDocs, but appear only in the Jump List for the Windows Explorer taskbar button. Folders do not appear in any other application’s Jump List.

  • LB · Edited

    One case where this doesn't work is if you need to mix different compiler ABI modes, e.g. /await vs /await:strict. What I did was have a public header with ABI-independent definitions (e.g. polymorphic virtual base classes) and then a private header that puts ifdefs around the namespace name to make it so each ABI variant has its own inline namespace. The private header implements the public interfaces one time and they end up in different inline namespaces automatically based on the compiler settings of whatever source file included it (to avoid ODR violations), and any ABI differences get type-erased out...

    Read more
  • Joshua Hudson

    I’m seeing it.

    In order for it to work, one of the following must be true:

    1) The logger is at the end of the class *and* all calls to the constructor are inside a replaceable method. Logically, this is the equivalent of a derived class. Clients never see a widget, they see a widget * or a widget & instead.

    2) The ifdef doesn’t cover the declaration of Logger, but only its initialization.

    • Raymond ChenMicrosoft employee Author

      Okay, but it does mean that everybody in the project is either using logging widgets or non-logging widgets. You can’t have some files using logging (say because you’re trying to debug a problem there), while other files don’t use logging.

      • Joshua Hudson · Edited

        Ok, to solve that problem, we have to take path 1. Path 2 won't work.

        We have a number of functions called CreateWidgetModuleXyz (where Xyz is the module name). CreateWidget is also weak.

        All of these are weak functions where the default implementation is a jump to CreateWidget or an alias for CreateWidget if your linker is smart enough. To activate the logger for a specific module, link in its module logger library. All of the libraries also provide a COMDEF definition of CreateWidget that allocates the space for the logger pointer and zeroes the pointer to it. The logger function itself...

        Read more