If you are using LuaTeX, you can use the new lua-ul package which also allows e.g. nested underlines:
\documentclass{article}
\usepackage{lua-ul,luacolor}
\usepackage{tikzducks,pict2e}
\newunderlinetype\myunderduck{\cleaders\hbox{%
\begin{tikzpicture}[x=.5ex,y=.5ex,baseline=.8ex]%
\duck
\end{tikzpicture}%
}}
\newunderlinetype\myunderwavy{\cleaders\hbox{%
\setlength\unitlength{.3ex}%
\begin{picture}(4,0)(0,1)
\thicklines
\color{red}%
\qbezier(0,0)(1,1)(2,0)
\qbezier(2,0)(3,-1)(4,0)
\end{picture}%
}}
\newcommand\underDuck[1]{{\myunderduck#1}}
\newcommand\underWavy[1]{{\myunderwavy#1}}
\begin{document}
V\underLine{A}V
\underDuck{VAV}
\underDuck{V}\underLine{AV}
\underDuck{These are \underWavy{ucks}}
\strikeThrough{Dinner is ready!}
\end{document}

Old answer (this later became lua-ul)
Using LuaTeX, you can use an attribute to mark the characters you want to underline. This does not interfere with kerning/ligaturing/line breaking because it only acts after all this is finished.
I added some extra flexibility for customized line thickness, placing, duck underlines, ... but the callback basically just iterates over the node tree and draws leaders whenever it finds a marked node:
\documentclass{article}
\usepackage{luacode}
\usepackage{tikzducks,pict2e}
\newattribute\underlineattr
\begin{luacode*}
local underlineattr = token.create'underlineattr'.index
local underline_types = {}
function new_underline_type()
table.insert(underline_types, tex.box[0].head)
tex.box[0].head = nil
tex.sprint(#underline_types)
end
local add_underline_h
local function add_underline_v(head)
for n in node.traverse(head) do
if head.id == node.id'hlist' then
add_underline_h(n)
elseif head.id == node.id'vlist' then
add_underline_v(n.head)
end
end
end
function add_underline_h(head)
node.slide(head.head)
local last_value
local first
for n in node.traverse(head.head) do
local new_value = node.has_attribute(n, underlineattr)
if n.id == node.id'hlist' then
new_value = nil
add_underline_h(n)
elseif n.id == node.id'vlist' then
new_value = nil
add_underline_v(n.head)
elseif n.id == node.id'kern' and n.subtype == 0 then
if n.next and not node.has_attribute(n.next, underlineattr) then
new_value = nil
else
new_value = last_value
end
elseif n.id == node.id'glue' and (
n.subtype == 8 or
n.subtype == 9 or
n.subtype == 15 or
false) then
new_value = nil
end
if last_value ~= new_value then
if last_value then
local width = node.rangedimensions(head, first, n)
local kern = node.new'kern'
kern.kern = -width
kern.next = node.copy(underline_types[last_value])
kern.next.width = width
kern.next.next = n
n.prev.next = kern
end
if new_value then
first = n
end
last_value = new_value
end
end
if last_value then
local width = node.rangedimensions(head, first)
local kern = node.new'kern'
kern.kern = -width
kern.next = node.copy(underline_types[last_value])
kern.next.width = width
node.tail(head.head).next = kern
end
end
local function filter(b, loc, prev, mirror)
add_underline_v(b)
local new_prev = mirror and b.height or b.depth
if prev > -65536000 then
local lineglue = tex.baselineskip.width - prev - (mirror and b.depth or b.height)
local skip
if lineglue < tex.lineskiplimit then
skip = node.new('glue', 1)
node.setglue(skip, node.getglue(tex.lineskip))
else
skip = node.new('glue', 2)
node.setglue(skip, node.getglue(tex.baselineskip))
skip.width = lineglue
end
skip.next = b
b = skip
end
return b, new_prev
-- return node.prepend_prevdepth(b)
end
luatexbase.callbacktypes.append_to_vlist_filter = 3 -- This should not be necessary
luatexbase.add_to_callback('append_to_vlist_filter', filter, 'add underlines to list')
\end{luacode*}
\newcommand\newunderlinetype[2]{%
\setbox0\hbox{#2\hskip0pt}%
\chardef#1=\directlua{new_underline_type()}\relax
}
\newunderlinetype\myunderline{\leaders\vrule height-1ptdepth1.5pt}
\newunderlinetype\mystrikethrough{\leaders\vrule height2.5ptdepth-2pt}
\newunderlinetype\myunderduck{\cleaders\hbox{%
\begin{tikzpicture}[baseline=3,scale=0.05]%
\duck
\end{tikzpicture}%
}}
\newunderlinetype\myunderwavy{\leaders\hbox{%
\setlength\unitlength{.3mm}%
\begin{picture}(4,0)(0,1)
\thicklines
\color{red}%
\qbezier(0,0)(1,1)(2,0)
\qbezier(2,0)(3,-1)(4,0)
\end{picture}%
}}
\newcommand\underLine[1]{{\underlineattr=\myunderline#1}}
\newcommand\underDuck[1]{{\underlineattr=\myunderduck#1}}
\newcommand\underWavy[1]{{\underlineattr=\myunderwavy#1}}
\newcommand\strikeThrough[1]{{\underlineattr=\mystrikethrough#1}}
\begin{document}
V\underLine{A}V
\underDuck{VAV}
\underDuck{V}\underLine{AV}
\underDuck{These are \underWavy{ucks}}
\strikeThrough{Dinner is ready!}
\end{document}

:-)Jokes aside, yes, of course. My desire of having this working is stronger than my distaste for stuff I don't understand...:-)– campa Aug 17 '18 at 15:32\foo{Kr}{a}{ft}with the pre, underlined, and post groups separate and possibly empty, then you don't have to mess with ifnextchar tests, for luatex you could perhaps use a callback to add the lines after typesetting – David Carlisle Aug 17 '18 at 15:35_active, and useKr_a_ft. With my current solution (plus the bit I haven't shown) spaces are not a problem, butßare. But I start to think that the question is ill-posed:-(– campa Aug 17 '18 at 15:38l\foo{ie}ßraises issues. Nothing that can't be cured byl\foo{ie}{ß}though. – campa Aug 17 '18 at 15:46