VHDL · Chapter 11.5 · Functions and Procedures
Operator and Subprogram Overloading
Overloading lets one name carry several definitions, with the compiler choosing the right one from the types and number of arguments, and sometimes the return type. It is why a single parity function can accept a standard logic vector or an unsigned value, and, crucially, why the plus operator works on unsigned and signed at all. An operator is just a function whose name is a quoted symbol, and the numeric standard package provides exactly those overloads. The compiler resolves each call by matching signatures, and when two definitions match equally well the call is ambiguous and fails, which is the classic symptom of importing two arithmetic packages at once. This lesson covers subprogram and operator overloading, how resolution works, and how to avoid ambiguity.
Foundation14 min readVHDLOverloadingOperatorsnumeric_stdSubprogramsResolution
1. Engineering intuition — one name, many implementations
It would be tedious if every type needed a differently-named operation: add_unsigned, add_signed,
parity_slv, parity_unsigned. Overloading removes that friction — you give all of them the same name and
let the compiler pick the right implementation from the argument types. This is not magic specific to a few
built-ins; it is how the whole numeric_std arithmetic layer works. a + b on two unsigned values calls an
overloaded "+" function defined for unsigned; the same + on signed calls a different one. Understanding
overloading explains where your operators come from and why mixing arithmetic packages breaks.
2. Formal explanation — overloaded subprograms and operators
-- SUBPROGRAM overloading: same name, distinguished by parameter types.
function parity (v : std_logic_vector) return std_logic; -- definition A
function parity (v : unsigned) return std_logic; -- definition B (same name, different type)
-- parity(slv_sig) → A ; parity(uns_sig) → B (resolved by the argument's type)
-- OPERATOR overloading: an operator IS a function whose name is a quoted symbol.
function "+" (l, r : unsigned) return unsigned; -- this is what numeric_std defines
function "+" (l, r : signed) return signed; -- and this
-- So 'a + b' is really '"+"(a, b)' — resolved by whether a,b are unsigned or signed.Overloading means several subprograms share a name, distinguished by the number and types of parameters (and
the return type). An operator is simply a function named with a quoted symbol ("+", "and", "="), so
operator overloading is the same mechanism — which is exactly how numeric_std adds arithmetic to unsigned/
signed.
3. Production usage — where overloads come from, and writing your own
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all;
-- numeric_std's overloaded "+" is why this works at all:
signal a, b, s : unsigned(7 downto 0);
s <= a + b; -- resolves to "+"(unsigned, unsigned) from numeric_std
-- Your own overloaded helper: one name, two types.
function max (l, r : integer) return integer is begin if l > r then return l; else return r; end if; end;
function max (l, r : unsigned) return unsigned is begin if l > r then return l; else return r; end if; end;
m_int <= max(x_int, y_int); -- integer version
m_uns <= max(x_uns, y_uns); -- unsigned version — same name, chosen by typeWhat hardware does this become? Whatever the selected definition synthesizes to: a + b on unsigned
inlines the numeric_std adder; max on unsigned inlines a comparator-plus-mux. Overloading is a compile-time
selection — the compiler resolves the name to one definition, then that definition inlines as usual. There is no
run-time dispatch; the choice is fixed at elaboration by the argument types.
4. Structural interpretation — resolution by signature
5. Why this is structural, not timing
Overloading is a compile-time name-resolution mechanism: the tool picks one definition from the argument types before any hardware exists, so a resolution diagram (above), not a waveform, captures it. The chosen definition then synthesizes and behaves exactly as that subprogram would on its own — there is no extra run-time cost or dispatch. The whole effect happens at elaboration; at run time there is just the one selected piece of logic.
6. Debugging example — the ambiguous overload
Expected: a + b on unsigned compiles. Observed: an error that the call is ambiguous / cannot
determine which "+" to use, or that the operands' type is unclear. Root cause: two equally-good overloads
are visible at once — the classic case is importing both ieee.numeric_std and the obsolete
ieee.std_logic_arith (or std_logic_unsigned), each defining "+", so neither is the unique best match.
Another cause is an integer literal that could match several numeric types. Fix: import only numeric_std
(drop the std_logic_arith family — lesson 10.5), and qualify or convert operands when a literal is genuinely
ambiguous. Engineering takeaway: an ambiguous-overload error means two definitions of the same name are in
scope — remove the duplicate package or pin the type with a conversion/qualified expression.
-- BUG: both packages define "+", so a + b is ambiguous.
-- use ieee.numeric_std.all; use ieee.std_logic_unsigned.all; -- two "+" on these operands
-- FIX: keep only numeric_std; one "+" → unambiguous.
use ieee.numeric_std.all; s <= a + b; -- a, b : unsigned7. Common mistakes & what to watch for
- Importing two arithmetic packages.
numeric_std+std_logic_arith/_unsignedgives duplicate"+"→ ambiguous. Usenumeric_stdonly. - Overloads that differ only by return type. Resolvable only when the context fixes the return type; prefer distinguishing by parameter types.
- Ambiguous literals. An integer literal may match several numeric types; qualify (
unsigned'(...)) or convert to pin it. - Over-overloading one name. Too many same-named variants hurt readability and invite ambiguity; overload only where it genuinely clarifies.
- Forgetting operators are functions. To give a custom type arithmetic/comparison, define
"+","=", etc. as overloaded functions.
8. Engineering insight & continuity
Overloading lets one name serve many types by resolving each call from its argument (and return) types at compile
time — the mechanism behind your own multi-type helpers and behind every numeric_std operator, and the source
of the ambiguous-overload error when two definitions collide. It is also the foundation for the next topic:
moving values between types. The standard library provides a family of overloaded conversion functions
(to_unsigned, to_integer, std_logic_vector(...), resize) for exactly that — the subject of the next
lesson, Conversion Functions.