Question
In C or C++, is there a faster way to test whether an integer x is inclusively between start and end than using this expression?
x >= start && x <= end
The target platform is iOS, and the test is used in a performance-sensitive box-blur routine that limits pixels to a circular region inside a square. How can this kind of range check be implemented efficiently while preserving correct behavior?
Short Answer
You will learn the standard inclusive range test, an unsigned-subtraction form that can reduce the test to one unsigned comparison, and the overflow, type, and side-effect rules needed to use it safely in C and C++. You will also learn why generated machine code and measurements matter more than assuming that fewer branches is always faster.
Concept
The normal inclusive range check is:
x >= start && x <= end
It is readable, correct when start <= end, and is usually the best default. Modern C and C++ compilers often optimize it very well.
When all values are handled as unsigned integers, an inclusive range can also be tested by subtracting the lower boundary:
x - start <= end - start
This works because unsigned arithmetic wraps modulo the type's range. If x is below start, then x - start wraps to a large value, which is larger than the width of an ordinary range. If x is inside the range, the subtraction produces its offset from start.
A convenient exclusive-upper-bound form is:
(x - start) < width
where:
width = (end - start) + 1;
This can often compile to a subtraction and one unsigned comparison. It may avoid a conditional branch in a hot loop, although the exact result depends on the compiler, target CPU, surrounding code, and optimization settings.
This technique matters in low-level, performance-sensitive code such as image processing, parsers, and numeric loops. However, it has important preconditions: use unsigned types deliberately, ensure the interval is valid, and ensure calculating the interval width does not wrap unexpectedly.
Mental Model
Think of the range as a measuring tape that begins at start.
x - startis the number of steps from the beginning of the tape tox.- Values in the range have offsets from
0throughend - start. - A value below
startproduces a negative offset. In unsigned arithmetic, that negative value wraps around into a huge positive number. - Therefore, one comparison can reject both values that are too low and values that are too high.
For example, with the range 10 through 15, valid offsets are 0 through 5:
x | x - 10 as unsigned | In range? |
|---|---|---|
| 9 | a very large value after wraparound | No |
| 10 | 0 | Yes |
| 13 | 3 | Yes |
| 15 | 5 | Yes |
| 16 | 6 | No |
Syntax and Examples
The clear, general-purpose version is:
bool in_range = x >= start && x <= end;
For an unsigned inclusive interval, use an offset and a width:
#include <cstdint>
bool isInRange(std::uint32_t x,
std::uint32_t start,
std::uint32_t end) {
if (start > end) {
return false;
}
std::uint32_t width = end - start + 1;
return x - start < width;
}
For start = 20 and end = 29, the width is 10. The expression x - 20 < 10 accepts offsets 0 to 9, corresponding to values 20 to 29.
A form that avoids calculating end - start + 1 is often easier to make safe:
bool isInRange(std::uint32_t x,
std::uint32_t start,
std::uint32_t end) {
return start <= end && x - start <= end - start;
}
The start <= end check is necessary if callers might provide an invalid range.
In C, the same idea works with fixed-width types from <stdint.h>:
#include <stdbool.h>
#include <stdint.h>
bool is_in_range(uint32_t x, uint32_t start, uint32_t end) {
return start <= end && x - start <= end - start;
}
Step by Step Execution
Consider this valid unsigned interval:
std::uint32_t start = 100;
std::uint32_t end = 104;
std::uint32_t x = 102;
bool inside = start <= end && x - start <= end - start;
Step by step:
start <= endbecomes100 <= 104, so it istrue.x - startbecomes102 - 100, which is2.end - startbecomes104 - 100, which is4.2 <= 4istrue.- Therefore,
insideistrue.
Now try a value below the range:
std::uint32_t x = 99;
bool inside = start <= end && x - start <= end - start;
start <= endis stilltrue.- Mathematically,
99 - 100is-1. - As an unsigned
uint32_t,-1wraps to4294967295. 4294967295 <= 4isfalse.- The value is correctly rejected without writing a separate
x >= starttest.
For a value above the range, such as x = 105, the offset is 5, and 5 <= 4 is also false.
Real World Use Cases
- Image processing: Check whether a pixel coordinate is within a row, column, tile, or clipped processing region.
- Buffers and arrays: Validate that an index is inside a known valid interval before accessing data.
- Binary protocols: Check whether a numeric field belongs to a permitted code range.
- Input validation: Accept IDs, scores, dates, or ports only when they lie within allowed limits.
- Game and graphics loops: Quickly test coordinates against a viewport, collision region, or texture bounds.
- Data processing: Filter values that belong to a bucket such as
1000through1999.
For a blur or other image filter, remember that a circle test normally also needs squared-distance math:
int dx = x - centerX;
int dy = y - centerY;
bool inCircle = dx * dx + dy * dy <= radius * radius;
Range checks are still useful for quickly limiting iteration to the circle's enclosing square before applying the more expensive circle test.
Real Codebase Usage
In most production code, developers begin with the readable form:
if (x < start || x > end) {
return;
}
This is a guard clause: invalid values leave early, keeping the main path simple.
For a hot inner loop where profiling shows the range test is important, a helper can document the unsigned technique:
#include <cstdint>
constexpr bool inInclusiveRange(std::uint32_t value,
std::uint32_t first,
std::uint32_t last) {
return first <= last && value - first <= last - first;
}
A real codebase should usually prefer a function or constexpr helper over a macro. The helper:
- Evaluates each argument once.
- Has a clear return type.
- Can be tested independently.
- Is easier for debuggers and static-analysis tools.
For loops over an image, it is often better to structure work so the loop boundaries already stay valid:
for (int y = top; y <= bottom; ++y) {
for (int x = left; x <= right; ++x) {
// x and y are already in the rectangular bounds.
}
}
This can eliminate repeated range checks entirely. Profile the complete algorithm: memory access patterns, cache behavior, repeated loads, and the circle calculation may matter more than one comparison.
Common Mistakes
Using signed subtraction when overflow is possible
This is not a safe general replacement for signed values:
// Risky for signed integers when subtraction can overflow.
bool inside = x - start <= end - start;
Signed integer overflow is undefined behavior in both C and C++. Convert to a suitable unsigned type only when unsigned modular semantics match the problem, or keep the normal comparisons.
Forgetting invalid ranges
If start > end, the interval is invalid. The subtraction technique alone may accidentally describe a different wrapped interval.
bool inside = start <= end && x - start <= end - start;
Include start <= end unless your program guarantees it elsewhere.
Overflow when calculating inclusive width
This can wrap when the interval includes every value of the unsigned type:
std::uint32_t width = end - start + 1; // Wraps to 0 if start is 0 and end is UINT32_MAX.
Prefer this form when full-range input is possible:
bool inside = start <= end && x - start <= end - start;
Adding side effects inside a macro
A macro like this is risky:
#define IN_RANGE(value, start, end) ((value) - (start) <= (end) - (start))
Calling IN_RANGE(i++, first, last) can increment i more than once because the macro substitutes value wherever it appears. Use a function instead.
Also, these two expressions do not have equivalent side effects:
p <= end && p++ >= start
(p++ - start) < width
The first increments p only when p <= end; the second increments p every time it is evaluated. Do not substitute one for the other unless that behavior is intentional and correct.
Assuming fewer source-level branches always means faster
The compiler may turn && into branchless instructions, or it may choose branches because they are faster for the surrounding code. Measure optimized builds on the actual target device and inspect compiler output only as supporting evidence.
Mixing signed and unsigned types accidentally
Comparing or subtracting mixed integer types can trigger implicit conversions that change results. Choose one explicit type, such as uint32_t for non-negative pixel coordinates, and use it consistently.
Comparisons
| Technique | Example | Best use | Important limitation |
|---|---|---|---|
| Two direct comparisons | x >= start && x <= end | Default application code | May become two comparisons, though compilers can optimize it |
| Unsigned offset comparison | x - start <= end - start | Profiled hot paths with unsigned values | Requires valid interval and deliberate unsigned semantics |
| Width / exclusive bound | x - start < width | When width is known and nonzero | width = end - start + 1 can wrap for a full unsigned range |
| Guard-clause form | `if (x < start | x > end) return;` | |
| Loop-bound design | Loop only from start to end | Repeated processing of a known region | Does not help when values arrive unpredictably |
The direct comparison and unsigned-offset comparison are logically equivalent for a valid unsigned interval. Prefer direct comparisons until profiling identifies this operation as a meaningful bottleneck.
Cheat Sheet
// Readable default, inclusive on both ends
bool inside = x >= start && x <= end;
// Safe unsigned offset form for possibly invalid ranges
bool inside = start <= end && x - start <= end - start;
// Exclusive upper bound: [start, start + width)
bool inside = x - start < width;
- Use the subtraction technique with unsigned integer types such as
uint32_t. - For inclusive
[start, end],end - startis the largest valid offset. - Check
start <= endif invalid ranges can occur. - Avoid
end - start + 1when a range might span the entire unsigned type. - Do not rely on signed overflow or mixed signed/unsigned conversions.
- Prefer an inline or
constexprfunction over a macro. - Never assume it is faster without measuring optimized code on the target device.
- Keep increments and other side effects outside range-test expressions when possible.
FAQ
Is x >= start && x <= end already fast in C++?
Usually, yes. It is clear and compilers optimize it effectively. Replace it only when profiling on the target build shows a real benefit.
Why does unsigned subtraction reject values below start?
Unsigned subtraction wraps modulo the type range. A value below start becomes a very large unsigned offset, which is larger than the interval width.
Can I use the unsigned trick with int?
Not as a general signed-arithmetic optimization. Signed overflow is undefined behavior. Use direct comparisons, or explicitly convert values only after verifying that unsigned semantics are appropriate.
Is the upper endpoint included?
Yes, with this expression:
x - start <= end - start
The offset for end is exactly end - start.
Why not use x - start < end - start + 1?
It is valid only when the + 1 does not wrap. For example, an interval from 0 through UINT32_MAX has a mathematical width of 2^32, which cannot fit in uint32_t.
Does a branchless expression always improve performance on iOS?
No. CPU architecture, compiler version, optimization level, data distribution, and surrounding memory operations all affect performance. Benchmark a representative workload on the actual device.
Why should I avoid a macro that increments p?
Macros can hide side effects and make evaluation order difficult to reason about. In addition, changing the condition can change whether p is incremented. Use explicit statements or a function whenever possible.
Mini Project
Description
Build a small coordinate filter for an image-processing-style task. It checks whether unsigned pixel coordinates belong to an inclusive rectangular region, then tests whether each coordinate is also inside a circle. This demonstrates safe range validation and avoids hidden side effects in macros.
Goal
Implement and test a function that accepts coordinates inside a rectangular clip area and a circular mask.
Requirements
Use std::uint32_t for non-negative coordinates and rectangle boundaries.
Create an inclusive range helper that safely handles first > last.
Create a function that checks whether a point is inside an inclusive rectangle.
Create a function that checks whether a point is inside a circle using squared distance.
Print whether several sample points pass both tests.
Keep learning
Related questions
2D Array Loop Order and Cache Performance in C
Learn why swapping nested loops changes 2D array performance in C, using row-major memory layout, cache locality, and practical benchmarks.
Array-to-Pointer Conversion in C and C++ Explained
Learn what array-to-pointer conversion means in C and C++, how array decay works, and how it differs from a pointer to an array.
Building More Fault-Tolerant Embedded C++ Applications for Radiation-Prone ARM Systems
Learn practical C++ and compile-time techniques to reduce soft-error damage in embedded ARM systems exposed to radiation.