skip to content

Why do two undo-stack operations that differ only in their type argument fail to compile as separate overloads?

level: middleimportance: should knowfreq 46%

answer

  1. selection compares published signatures
  2. the argument is not part of one
  3. both parameter shapes become identical
  4. one signature declared twice
  5. error at the declaration, not the call

basics

~20 s

Once the argument is discarded, both operations expose the same parameter shape, so the two declarations collapse into one signature. The clash is a duplicate declaration, reported where the operations are declared rather than at any call.

solid answer

~40 s

Overload selection matches a call against the **parameter shapes** the declarations expose, and those shapes are what compilation actually produces. Two operations named alike, one taking a batch of text edits and one taking a batch of style edits, differ in source only by the argument inside the parameter type. Strip the argument and both read `takes a batch` - the same name, the same number of parameters, the same shapes. That is one signature declared twice, and a type cannot declare the same signature twice. The error therefore lands on the pair of declarations, not on any call site: it is a property of the type, reported the moment the type is compiled, even if nothing ever calls either operation. Distinct names, or a parameter that genuinely differs, are what separate them again.

code

pseudocode · 6 lines
pseudocode
type EditLog:
    function record(batch: Batch<TextEdit>):  ...
    function record(batch: Batch<StyleEdit>): ...

# after the arguments are dropped, both declarations read:
#     record(batch: Batch)   ->  the same signature, declared twice

go deeper

for a junior

The takeaway is simple: two operations sharing a name must differ in their parameters, and a difference that exists only inside a type argument disappears before that comparison is made.

for a middle

Be able to run the derivation out loud - argument dropped, shapes equal, signature duplicated - and to say that the error is reported at the declarations, not at any call, even if nothing ever calls them.

for a senior

Show that you would catch this at design time: an API whose two entry points differ only in an element type is telling you the name is overloaded with meaning, and renaming is the fix rather than a trick to sneak both past the compiler.

for a principal

The standard worth setting is that public entry points are distinguished by name, not by a subtlety of the type system, so that indirect and generated callers can select them as reliably as direct ones.

## What overload selection actually compares **Overloading** means several operations share a name and are told apart by their parameters. The mechanism has two halves, and only the first one matters here: - **At the declaration**, a type publishes a set of signatures, and no two may be the same. A signature is the name plus the sequence of parameter shapes. - **At a call**, the compiler matches the supplied arguments against that set and picks one. Both halves work on the shapes the declarations expose. So the question 'do these two collide?' is settled entirely by what the parameter types look like after compilation, and never by what a caller happens to pass. ## Two declarations, one signature Take an edit log with two operations for recording a batch: one declared to take a batch of text edits, one a batch of style edits. In source they are clearly different - the argument inside the parameter type differs. In a model that discards type arguments, the parameter shape that survives is *a batch*, with nothing attached. Run the derivation in order: 1. Each declaration's parameter type is a container plus an argument. 2. Compilation keeps the container and drops the argument. 3. Both parameter shapes become the same container shape. 4. Both declarations therefore publish the identical name-plus-shapes signature. 5. Publishing one signature twice is a duplicate declaration, which is refused. The interesting step is 3, and the reason candidates get it wrong is that they picture step 5 happening at a call. It does not. ## Where the error lands This is the part that separates a derived answer from a remembered one. The clash is a property of **the type being declared**: - It is reported while compiling the declarations, before any caller is considered. - It is reported even if the two operations are never called anywhere in the program. - It is *not* an ambiguity - an ambiguity is a call that matches two distinct signatures, and here there are not two signatures to be ambiguous between. - Making the bodies differ, or documenting which is which, changes nothing, because neither is part of the signature. ## Which pairs collide and which survive | The two declarations | Shapes after the argument is dropped | Outcome | |---|---|---| | batch of text edits / batch of style edits | batch / batch | collide - one signature twice | | batch of text edits / a single edit | batch / edit | distinct, both compile | | batch of edits / batch of edits plus a limit | batch / batch and number | distinct, both compile | | batch of text edits / a differently named operation | different names | distinct, both compile | The table is the whole rule: what survives compilation is what distinguishes, and an argument does not survive. ## Getting out of it There are two honest ways out, and one that only looks like a way out. - **Give the operations distinct names.** Usually the best outcome anyway: if the two bodies really do different work on different edit families, the name was carrying information the signature could not. - **Make the parameter lists genuinely differ** - a different number of parameters, or a parameter whose shape survives. - **Not a way out: differing only in what each returns.** What a call is matched against is the parameter side, so a difference on the result side does not separate two declarations that already publish the same signature. ## Why the language does this rather than something cleverer A compiler could, in principle, keep the declarations apart and pick between them using information available only in source. It would then have two operations that the running program cannot tell apart at all: anything that reaches them indirectly, by name, would have no way to choose. Refusing the pair at the declaration keeps one rule instead of two - *what the program can distinguish is what the compiler will distinguish* - and it reports the problem to the person writing the type, who can still rename, rather than to the person calling it, who cannot. That framing is the answer an interviewer is listening for: the restriction is not an arbitrary quirk of overloading but a direct consequence of the signature being the only thing the program has, once the argument is gone.

  • Is this an ambiguity that a caller could resolve by being more explicit?
    No. An ambiguity means a call matches two distinct signatures and the compiler cannot choose. Here the two declarations publish the same signature, so there is nothing for a caller to choose between and no amount of explicitness at the call site helps. The pair is invalid before any call is examined.
  • Why does adding a parameter of a different shape fix it when changing the argument does not?
    Because the added parameter survives compilation and the argument does not. Signatures are compared on the shapes that actually reach the published form, so a difference that exists only in source cannot separate them, while a difference in the number or shape of parameters can.

saying these in an interview costs you the question

  • Says the compiler picks by inspecting values passed at run time.
  • Thinks the error appears at a call site as an ambiguity.
  • Believes differing results are enough to separate the declarations.
  • Claims a cast at the call would let the compiler choose.
  • Assumes the bodies differing makes the declarations different.