diff --git a/README.md b/README.md
index 41e491b4..224a35da 100644
--- a/README.md
+++ b/README.md
@@ -84,6 +84,15 @@ int main()
 }
 ```
 
+Compatibility Guidelines
+========================
+
+The users of cppzmq are expected to follow the guidelines below to ensure not to break when upgrading cppzmq to newer versions (non-exhaustive list):
+
+* Do not depend on any macros defined in cppzmq unless explicitly declared public here.
+
+The following macros may be used by consumers of cppzmq: `CPPZMQ_VERSION`, `CPPZMQ_VERSION_MAJOR`, `CPPZMQ_VERSION_MINOR`, `CPPZMQ_VERSION_PATCH`.
+
 Contribution policy
 ===================
 
diff --git a/tests/buffer.cpp b/tests/buffer.cpp
index 9520201a..da8eaddc 100644
--- a/tests/buffer.cpp
+++ b/tests/buffer.cpp
@@ -222,7 +222,7 @@ TEST_CASE("mutable_buffer creation string", "[buffer]")
     CHECK(b2.data() == d.data());
 }
 
-#ifdef ZMQ_CPP17
+#if CPPZMQ_HAS_STRING_VIEW
 TEST_CASE("const_buffer creation string_view", "[buffer]")
 {
     std::wstring dstr(10, L'a');
diff --git a/tests/message.cpp b/tests/message.cpp
index 75d28c39..aef0ae50 100644
--- a/tests/message.cpp
+++ b/tests/message.cpp
@@ -158,7 +158,7 @@ TEST_CASE("message to string", "[message]")
     const zmq::message_t b("Foo", 3);
     CHECK(a.to_string() == "");
     CHECK(b.to_string() == "Foo");
-#ifdef ZMQ_CPP17
+#if CPPZMQ_HAS_STRING_VIEW
     CHECK(a.to_string_view() == "");
     CHECK(b.to_string_view() == "Foo");
 #endif
diff --git a/tests/socket.cpp b/tests/socket.cpp
index d56a7bf8..ec40536e 100644
--- a/tests/socket.cpp
+++ b/tests/socket.cpp
@@ -69,7 +69,7 @@ TEST_CASE("socket options", "[socket]")
     socket.set(zmq::sockopt::routing_id, "foobar");
     socket.set(zmq::sockopt::routing_id, zmq::buffer(id));
     socket.set(zmq::sockopt::routing_id, id);
-#ifdef ZMQ_CPP17
+#if CPPZMQ_HAS_STRING_VIEW
     socket.set(zmq::sockopt::routing_id, std::string_view{id});
 #endif
 
diff --git a/zmq.hpp b/zmq.hpp
index 260d5aa1..fe966044 100644
--- a/zmq.hpp
+++ b/zmq.hpp
@@ -103,18 +103,29 @@
 #include <tuple>
 #include <memory>
 #endif
-#ifdef ZMQ_CPP17
-#ifdef __has_include
-#if __has_include(<optional>)
-#include <optional>
-#define ZMQ_HAS_OPTIONAL 1
+
+#if defined(__has_include) && defined(ZMQ_CPP17)
+#define CPPZMQ_HAS_INCLUDE_CPP17(X) __has_include(X)
+#else
+#define CPPZMQ_HAS_INCLUDE_CPP17(X) 0
 #endif
-#if __has_include(<string_view>)
-#include <string_view>
-#define ZMQ_HAS_STRING_VIEW 1
+
+#if CPPZMQ_HAS_INCLUDE_CPP17(<optional>) && !defined(CPPZMQ_HAS_OPTIONAL)
+#define CPPZMQ_HAS_OPTIONAL 1
 #endif
+#ifndef CPPZMQ_HAS_OPTIONAL
+#define CPPZMQ_HAS_OPTIONAL 0
+#elif CPPZMQ_HAS_OPTIONAL
+#include <optional>
 #endif
 
+#if CPPZMQ_HAS_INCLUDE_CPP17(<string_view>) && !defined(CPPZMQ_HAS_STRING_VIEW)
+#define CPPZMQ_HAS_STRING_VIEW 1
+#endif
+#ifndef CPPZMQ_HAS_STRING_VIEW
+#define CPPZMQ_HAS_STRING_VIEW 0
+#elif CPPZMQ_HAS_STRING_VIEW
+#include <string_view>
 #endif
 
 /*  Version macros for compile-time API version detection                     */
@@ -582,7 +593,7 @@ class message_t
     {
         return std::string(static_cast<const char *>(data()), size());
     }
-#ifdef ZMQ_CPP17
+#if CPPZMQ_HAS_STRING_VIEW
     // interpret message content as a string
     std::string_view to_string_view() const noexcept
     {
@@ -830,7 +841,7 @@ struct recv_buffer_size
     }
 };
 
-#if defined(ZMQ_HAS_OPTIONAL) && (ZMQ_HAS_OPTIONAL > 0)
+#if CPPZMQ_HAS_OPTIONAL
 
 using send_result_t = std::optional<size_t>;
 using recv_result_t = std::optional<size_t>;
@@ -1237,7 +1248,7 @@ const_buffer buffer(const std::basic_string<T, Traits, Allocator> &data,
     return detail::buffer_contiguous_sequence(data, n_bytes);
 }
 
-#if defined(ZMQ_HAS_STRING_VIEW) && (ZMQ_HAS_STRING_VIEW > 0)
+#if CPPZMQ_HAS_STRING_VIEW
 // std::basic_string_view
 template<class T, class Traits>
 const_buffer buffer(std::basic_string_view<T, Traits> data) noexcept
@@ -1662,7 +1673,7 @@ class socket_base
         set_option(Opt, buf.data(), buf.size());
     }
 
-#ifdef ZMQ_CPP17
+#if CPPZMQ_HAS_STRING_VIEW
     // Set array socket option, e.g.
     // `socket.set(zmq::sockopt::routing_id, id_str)`
     template<int Opt, int NullTerm>