As a minimal example (this is part of a larger project), I have the following three files in a project with the following structure:
.
├── src
│ ├── binary_search.cpp
└── test
├── CMakeLists.txt
├── build
└── test_binary_search.cpp
binary_search.cpp:
#include <cstdint>
template <typename T> T binary_search(T A[], uint8_t len, T nu) {
uint8_t low = 0;
uint8_t high = len - 1;
while (low <= high) {
uint8_t mid = low + (high - low) / 2;
if (nu == A[mid])
return mid;
else if (nu > A[mid])
low = mid + 1;
else
high = mid - 1;
}
return -1;
}
test_binary_search.cpp:
#include <boost/test/data/test_case.hpp>
#include <boost/test/included/unit_test.hpp>
#define BOOST_TEST_MODULE test_algorithms
BOOST_AUTO_TEST_SUITE(test_search)
int32_t input_arrs[4][8] = {{26, 31, 41, 41, 58, 59, 101, 104},
{26, 31, 41, 41, 58, 59, 101, 104},
{1, 4, 5, 7, 19, 28, 45, 92},
{1, 4, 5, 7, 19, 28, 45, 92}};
int32_t input_vals[] = {31, 32, 92, 101};
int32_t expected_vals[] = {1, -1, 7, -1};
auto test_cases = boost::unit_test::data::make(input_arrs) ^
boost::unit_test::data::make(input_vals) ^
boost::unit_test::data::make(expected_vals);
BOOST_DATA_TEST_CASE(test_binary_search, test_cases, input_arr, nu, exp) {
auto obs = binary_search(input_arr, 8, nu);
BOOST_ASSERT(obs == exp);
}
CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -std=c++20")
project(test_algorithms)
set(SOURCES
../src/binary_search.cpp
test_binary_search.cpp
)
find_package(Boost 1.87.0 REQUIRED COMPONENTS unit_test_framework)
if (Boost_FOUND)
enable_testing()
include_directories(${Boost_INCLUDE_DIRS})
add_executable(test_binary_search ${SOURCES})
target_include_directories(test_binary_search PRIVATE ${Boost_INCLUDE_DIRS})
add_test(NAME test_binary_search COMMAND test_binary_search)
endif()
I am attempting to compile this on a MacBook Pro M3 Pro running MacOS Sequoia 15.3.1 using the following commands:
cmake -S . -B build
cmake --build build
after which I receive a very long error message. I suspect that the problem might be
/Users/graham/dev/cpp_experiments/test/test_binary_search.cpp:20:14: error: use of undeclared identifier 'binary_search'
20 | auto obs = binary_search(input_arr, 8, nu);
which may in turn be caused by
/opt/homebrew/include/boost/test/data/test_case.hpp:89:16: error: reference to type 'const const int[8]' could not bind to an lvalue of type 'const int'
89 | return *value;
I'm not sure where/how I'm casting to type const int
in this code; are the Boost macros are performing this conversion? I'm struggling to understand the recommended way to use boost::unit_test::data::make
to zip up the parameterized test data. Any help in how to better structure this code to create a parameterized test suite would be greatly appreciated!
Apparently the element types need to be movable, which isn't true for T(&)[N]
. You could replace it with std::array
.
Next you will find that you need to make the type printable.
Aside from this, there are numerous issues with the code:
binary_search.cpp
defines a template. Its definition MUST be available at place of invocation, so it has to be an include.
I'd suggest renaming
binary_search.cpp
tobinary_search.hpp
or to split into a header/source where all template definitions reside in the header (Why can templates only be implemented in the header file?)
binary_search
collides with a standard library algorithm name. This can (will!) lead to surprises when you least expect it (e.g. if you pass the std::array<>
directly, ADL will find std::binary_search
in favor of your own.
I suggest renaming your algorithm to
my_binary_search
or making it a member of a namespace so you can fully qualify it for clarity
Your interface takes T A[]
which is equivalent to T* A
. Also, it's an antipattern in C++ to pass raw pointers with separate size. Consider specifying it as std::span<T>
instead with no overhead. Now you don't have to do the awkward, error-prone copy to a temporary array either
Your interface hard-codes uint8_t
for len
. This is a recipe for truncation/conversion errors. Worse, passing -1
will silently behave as if len==255
was passed.
You used BOOST_ASSERT
which is not part of the Boost Test library, and compiles away in release build (when NDEBUG
is defined). You probably wanted BOOST_CHECK
or, indeed, BOOST_CHECK_EQUAL
here. If you compile with warnings enabled you will actually be informed of mistakes like these (because obs
and exp
were never used in release mode).
you were missing BOOST_AUTO_TEST_SUITE_END()
you should probably supply link-dependencies instead of manually specifying boost include-directories in cmake:
target_link_libraries(test_binary_search Boost::unit_test_framework)
Since you know you don't need runtime link dependencies, you can use the headers
target to just get the includes:
target_link_libraries(test_binary_search Boost::headers)
also, using target-specific flags/libraries etc. obviates the need for include_directories
on my Boost version (a develop version after 1.88) there are deprecated header warnings, so I'll define BOOST_ALLOW_DEPRECATED_HEADERS
Given all these, here's my counter-proposition:
File src/binary_search.cpp
#include "binary_search.hpp"
// implementations of non-template functions here
File src/binary_search.hpp
#pragma once
#include <cstdint>
namespace mylib {
template <typename T> T my_binary_search(T A[], uint8_t len, T nu) {
uint8_t low = 0;
uint8_t high = len - 1;
while (low <= high) {
uint8_t mid = low + (high - low) / 2;
if (nu == A[mid])
return mid;
else if (nu > A[mid])
low = mid + 1;
else
high = mid - 1;
}
return -1;
}
} // namespace mylib
#include <span>
namespace mylib {
template <typename T, std::size_t N, typename V> //
T sehe_binary_search(std::span<T, N> a, V const& nu) {
uint8_t low = 0;
uint8_t high = a.size() - 1;
while (low <= high) {
uint8_t mid = low + (high - low) / 2;
if (nu == a[mid])
return mid;
else if (nu > a[mid])
low = mid + 1;
else
high = mid - 1;
}
return -1;
}
} // namespace mylib
File test/test_binary_search.cpp
#define BOOST_TEST_MODULE test_algorithms
#include "../src/binary_search.hpp"
#include <boost/test/data/test_case.hpp>
#include <boost/test/included/unit_test.hpp>
#include <boost/test/included/unit_test_framework.hpp>
#include <boost/test/tools/output_test_stream.hpp>
#include <cstdint>
namespace utf = boost::unit_test;
using Arr8 = std::array<int32_t, 8>;
// make streamable for boost test:
namespace std {
std::ostream& boost_test_print_type(std::ostream& os, Arr8 const& arr) {
os << "{";
for (char const* sep = ""; auto const& el : arr)
os << std::exchange(sep, ", ") << el;
return os << "}";
}
} // namespace std
BOOST_AUTO_TEST_SUITE(test_search)
namespace { // file static visibility
std::array<Arr8, 4> constexpr input_arrs{{
{26, 31, 41, 41, 58, 59, 101, 104},
{26, 31, 41, 41, 58, 59, 101, 104},
{1, 4, 5, 7, 19, 28, 45, 92},
{1, 4, 5, 7, 19, 28, 45, 92},
}};
int32_t constexpr input_vals[] = {31, 32, 92, 101};
int32_t constexpr expected_vals[] = {1, -1, 7, -1};
auto test_cases = //
utf::data::make(input_arrs) ^ //
utf::data::make(input_vals) ^ //
utf::data::make(expected_vals);
} // namespace
BOOST_DATA_TEST_CASE(my_binary_search, test_cases, input_arr, nu, exp) {
int32_t tmp[8];
std::copy(std::begin(input_arr), std::end(input_arr), std::begin(tmp));
auto obs = mylib::my_binary_search(tmp, 8, nu);
BOOST_CHECK_EQUAL(obs, exp);
}
BOOST_DATA_TEST_CASE(sehe_binary_search, test_cases, input_arr, nu, exp) {
auto obs = mylib::sehe_binary_search(std::span(input_arr), nu);
BOOST_CHECK_EQUAL(obs, exp);
}
BOOST_AUTO_TEST_SUITE_END()
File test/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
set(CMAKE_EXPORT_COMPILE_COMMANDS On)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -std=c++20 -pedantic -Wall -Wextra -Werror")
project(test_algorithms)
set(SOURCES
../src/binary_search.cpp
test_binary_search.cpp
)
find_package(Boost 1.87.0 REQUIRED COMPONENTS unit_test_framework)
if (Boost_FOUND)
enable_testing()
add_executable(test_binary_search ${SOURCES})
target_link_libraries(test_binary_search Boost::headers)
target_compile_definitions(test_binary_search PRIVATE BOOST_ALLOW_DEPRECATED_HEADERS)
add_test(NAME test_binary_search COMMAND test_binary_search)
endif()
With a live demo: