c++language-lawyersemanticsoverload-resolution

Can the semantics of a translation unit depend on a function declaration that is never used?


Can the semantics of a well-formed C++ translation unit depend on the presence of a function or function template declaration that is never used? By "used", I mean it is selected by overload resolution, is implicitly or explicitly specialized (if a template), or is a definition and has external linkage.

For example, suppose I have something like:

static int f(int)   { return 1; }
static int f(float) { return 2; }
static int f(char)  { return 3; }
int main()
{
  return f(9);     // returns 1
}

and then I delete one or both of the non-selected overloads:

static int f(int)   { return 1; }
int main()
{
  return f(9);     // returns 1
}

can the resulting translation unit have different semantics than the original? "Different semantics" could mean that it is now ill-formed (whether or not a diagnostic is required), or that it is well-formed but the set of potential run-time behaviors is different.

Intuitively, it seems like the answer should be no, and a straightforward reading of the relevant standard passages (focusing on selection by overload resolution as the usage method) seems to confirm this. Specifically, C++17 16.3 pp2-3 says:

(p2) Overload resolution selects the function to call in seven distinct contexts within the language:

[...]

Each of these contexts defines the set of candidate functions and the list of arguments in its own unique way. But, once the candidate functions and argument lists have been identified, the selection of the best function is the same in all cases:

  • (2.8) First, a subset of the candidate functions (those that have the proper number of arguments and meet certain other conditions) is selected to form a set of viable functions (16.3.2).

  • (2.9) Then the best viable function is selected based on the implicit conversion sequences (16.3.3.1) needed to match each argument to the corresponding parameter of each viable function.

(p3) If a best viable function exists and is unique, overload resolution succeeds and produces it as the result. Otherwise overload resolution fails and the invocation is ill-formed. When overload resolution succeeds, and the best viable function is not accessible (Clause 14) in the context in which it is used, the program is ill-formed.

Then, 16.3.3 p2 says:

If there is exactly one viable function that is a better function than all other viable functions, then it is the one selected by overload resolution; otherwise the call is ill-formed.

This procedure seems to have the property that the best viable function F from a set S, if it exists, will be the same as for any subset of S that contains F, because F must have been better than every other element in S, so it would also be better than all other functions in any subset of S.

And if that is true, then I think that means that deleting a declaration that is never selected by overload resolution cannot alter the semantics of a well-formed translation unit, since it could only cause each overload resolution that is performed to happen with a subset of the original set of candidates, and hence yield the same result.

But, perhaps there is a way that the presence of a declaration, which ultimately does not get selected, nevertheless affects the set of candidates, maybe by way of argument-dependent lookup? The full lookup rules are quite complex. Or, what if an unused declaration has a default argument expression that triggers a template instantiation that manages to affect the semantics elsewhere (without undefined behavior)?

This sort of question, asking to prove a negative, is inherently difficult to answer. The ideal answer would be either a counterexample or a reference to somewhere in the standard or the committee's rationale (do they even do that anymore?) clarifying that the language is intended to have the property that deleting an unused declaration cannot affect semantics. Alternatively, I'd likely accept a reasonably well-researched answer exploring other angles (like the candidate inhibition and default argument ideas) and arriving at a negative conclusion, similar to what I have above.

Note: The essence of this question relates to functions that are "unused" according to a thorough but fundamentally syntactic examination of the AST after parsing and semantic analysis is finished. I have chosen to formalize "used" as "selected by overload resolution" (or is specialized, or is a definition with external linkage) since I think that captures almost every case of usage, but there are a few exceptions; for example, there are implementation-defined things like the GCC constructor attribute that lead to a call, and again should be treated as a use. What I'm after, in an answer, is information about cases where there is no apparent use of a function, even when taking into account perhaps obscure but nevertheless straightforward mechanisms (especially when they cause the function to be called), but the semantics is affected by its presence.

I tried several searches but didn't find anything relevant, in part because there are so many hits for things only tangentially related.

(The motivation for this question comes from a project to build a dependency analysis that would, among other things, automatically delete unused declarations. I'd like to be sure that I can do so without altering the meaning of the program.)


Based on HolyBlackCat's answer, I constructed this example (I'm most interested in the case where the unused function declaration is not a definition, but either way is relevant):

// unused.cc
// Unused function causes effect.

int globalVar = 0;

// Class whose constructor has a side effect.
struct Ctor {
  Ctor() {
    ++globalVar;
  }
  static int f() { return 1; }
};

// Template class with static data whose ctor has a side effect.
template <class T>
struct S {
  static Ctor ctor;
};

template <class T>
Ctor S<T>::ctor;

// Default argument expression instantiates S and ensures its 'ctor'
// member is instantiated.  But this function is never used.
static void unused(int x = S<int>::ctor.f());

int main()
{
  // Returns 1 if the side effect happens.
  return globalVar;
}

// EOF

But I find that clang and gcc disagree on its output, with clang returning 0 and gcc returning 1: https://godbolt.org/z/s1vP3fe11 .

The ideal counterexample would be unambiguously well-formed with both clang and gcc agreeing on its semantics...


Solution

  • Your definition of "used" is too loose to catch all edge cases. Here's a few examples of removing "unused" functions affecting behaviour.

    1. ADL requires the correct type of name be found, so even though ::f is never called, it is necessary.
    namespace N
    {
        struct S{};
    
        template<typename>
        void f(S);
    }
    
    // error if removed
    template<typename>
    void f(int) = delete;
    
    void bar()
    {
        f<void>(N::S{});
    }
    
    1. SFINAE with overload resolution. Neither f is selected in the original program, but they affect which g is selected.
    // error if removed
    void f(int);
    void f(const int&);
    
    template<typename T = int>
    auto g(int) -> decltype(f(T{})) = delete;
    void g(char);
    
    void bar()
    {
        g(0);
    }
    
    1. By implicitly instantiating templates (mentioned in HolyBlackCat's answer)
    template<int>
    struct flag
    {
        friend constexpr int adl(flag);
    };
    
    template<int n>
    struct write
    {
        friend constexpr int adl(flag<n>) { return n; }
    };
    
    // error if removed
    write<0> f()
    {
        return {};
    }
    constexpr int i = adl(flag<0>{});