diff --git a/lmorpho/morphio.cpp b/lmorpho/morphio.cpp
index 4c50fb6b77178e3466ae2937dd40b13b18eaaccd..47fc16f7b9313ef26964f13a1557b4fbde0e44f1 100644
--- a/lmorpho/morphio.cpp
+++ b/lmorpho/morphio.cpp
@@ -6,32 +6,15 @@
 
 #include <morphology.hpp>
 #include <swcio.hpp>
+#include <util/strprintf.hpp>
 
 #include "morphio.hpp"
 
 using nest::mc::io::swc_record;
+using nest::mc::util::strprintf;
 
 std::vector<swc_record> as_swc(const nest::mc::morphology& morph);
 
-// printf wrappers.
-
-template <typename... Args>
-static std::string strprintf(const char* fmt, Args&&... args) {
-    thread_local static std::vector<char> buffer(1024);
-
-    for (;;) {
-        int n = std::snprintf(buffer.data(), buffer.size(), fmt, std::forward<Args>(args)...);
-        if (n<0) return ""; // error
-        if ((unsigned)n<buffer.size()) return std::string(buffer.data());
-        buffer.resize(2*n);
-    }
-}
-
-template <typename... Args>
-static std::string strprintf(std::string fmt, Args&&... args) {
-    return strprintf(fmt.c_str(), std::forward<Args>(args)...);
-}
-
 // Multi-file manager implementation.
 multi_file::multi_file(const std::string& pattern, int digits) {
     auto npos = std::string::npos;
diff --git a/src/util/strprintf.hpp b/src/util/strprintf.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..218d38bc2a21871197aa2dddaad32ce65386cf34
--- /dev/null
+++ b/src/util/strprintf.hpp
@@ -0,0 +1,52 @@
+#pragma once
+
+/* Thin wrapper around std::snprintf for sprintf-like formatting
+ * to std::string. */
+
+#include <cstdio>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace nest {
+namespace mc {
+namespace util {
+
+namespace impl {
+    template <typename X>
+    X sprintf_arg_translate(const X& x) { return x; }
+
+    inline const char* sprintf_arg_translate(const std::string& x) { return x.c_str(); }
+
+    template <typename T, typename Deleter>
+    T* sprintf_arg_translate(const std::unique_ptr<T, Deleter>& x) { return x.get(); }
+
+    template <typename T>
+    T* sprintf_arg_translate(const std::shared_ptr<T>& x) { return x.get(); }
+}
+
+template <typename... Args>
+std::string strprintf(const char* fmt, Args&&... args) {
+    thread_local static std::vector<char> buffer(1024);
+
+    for (;;) {
+        int n = std::snprintf(buffer.data(), buffer.size(), fmt, impl::sprintf_arg_translate(std::forward<Args>(args))...);
+        if (n<0) {
+            throw std::system_error(errno, std::generic_category());
+        }
+        else if ((unsigned)n<buffer.size()) {
+            return std::string(buffer.data(), n);
+        }
+        buffer.resize(2*n);
+    }
+}
+
+template <typename... Args>
+std::string strprintf(const std::string& fmt, Args&&... args) {
+    return strprintf(fmt.c_str(), std::forward<Args>(args)...);
+}
+
+} // namespace util
+} // namespace mc
+} // namespace nest
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index eb2170e5e7601a77b862132124fa9a51e2ba7488..2aa34ffb8987dfb36b48f1c7bd29e4f7314d782e 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -60,6 +60,7 @@ set(TEST_SOURCES
     test_spikes.cpp
     test_spike_store.cpp
     test_stimulus.cpp
+    test_strprintf.cpp
     test_swcio.cpp
     test_synapses.cpp
     test_tree.cpp
diff --git a/tests/unit/test_strprintf.cpp b/tests/unit/test_strprintf.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..781ea1b0f446f711b62981587a23282a1c91d0ec
--- /dev/null
+++ b/tests/unit/test_strprintf.cpp
@@ -0,0 +1,63 @@
+#include <cstdio>
+#include <memory>
+#include <string>
+
+#include "../gtest.h"
+#include <util/strprintf.hpp>
+
+using namespace nest::mc::util;
+
+
+TEST(strprintf, simple) {
+    char buf[200];
+
+    const char* fmt1 = " %% %04d % 3.2f %#016x %c";
+    sprintf(buf, fmt1, 3, 7.1e-3, 0x1234ul, 'x');
+    auto result = strprintf(fmt1, 3, 7.1e-3, 0x1234ul, 'x');
+    EXPECT_EQ(std::string(buf), result);
+
+    const char* fmt2 = "%5s %3s";
+    sprintf(buf, fmt2, "bear", "pear");
+    result = strprintf(fmt2, "bear", "pear");
+    EXPECT_EQ(std::string(buf), result);
+}
+
+TEST(strprintf, longstr) {
+    std::string aaaa(2000, 'a');
+    std::string bbbb(2000, 'b');
+    std::string x;
+
+    x = aaaa;
+    ASSERT_EQ(x, strprintf("%s", x.c_str()));
+
+    x += bbbb+x;
+    ASSERT_EQ(x, strprintf("%s", x.c_str()));
+
+    x += bbbb+x;
+    ASSERT_EQ(x, strprintf("%s", x.c_str()));
+
+    x += bbbb+x;
+    ASSERT_EQ(x, strprintf("%s", x.c_str()));
+
+    x += bbbb+x;
+    ASSERT_EQ(x, strprintf("%s", x.c_str()));
+}
+
+TEST(strprintf, wrappers) {
+    // can we print strings and smart-pointers directly?
+
+    char buf[200];
+
+    auto uptr = std::unique_ptr<int>{new int(17)};
+    sprintf(buf, "uptr %p", uptr.get());
+
+    EXPECT_EQ(std::string(buf), strprintf("uptr %p", uptr));
+
+    auto sptr = std::shared_ptr<double>{new double(19.)};
+    sprintf(buf, "sptr %p", sptr.get());
+
+    EXPECT_EQ(std::string(buf), strprintf("sptr %p", sptr));
+
+    EXPECT_EQ(std::string("fish"), strprintf("fi%s", std::string("sh")));
+}
+